// ENVIRONMENT require('dotenv').config(); // DATABASE const { PrismaClient : CMSClient } = require("../../prisma/clients/cms"); const prisma = new CMSClient(); // CONSTANTS const { badRequestResponse, successResponse, errorResponse } = require("../res/responses.js"); const { localTime } = require("../services/time.services.js"); exports.reportCrash = async (req, res) => { try { const { appId, appVersion, buildVersion, crashId, sessionId, userId, crashType, exceptionName, exceptionReason, stackTrace, threadName, isFatal, severity, deviceModel, deviceBrand, osName, osVersion, architecture, availableRam, totalRam, availableDisk, totalDisk, batteryLevel, isRooted, isDebugger, networkType, breadcrumbs, customData, logs } = req.body; if (!appId || !appVersion || !crashId || !crashType) { return badRequestResponse(res, "Missing required fields: appId, appVersion, crashId, crashType"); } await prisma.$transaction(async (tx) => { let existingCrash = await tx.crashReport.findUnique({ where: { CrashId_CR: crashId } }); if (!existingCrash && exceptionName && stackTrace) { existingCrash = await tx.crashReport.findFirst({ where: { AppId_CR: appId, AppVersion_CR: appVersion, ExceptionName_CR: exceptionName, StackTrace_CR: stackTrace, CrashType_CR: crashType }, orderBy: { CreatedAt_CR: 'desc' } }); } if (existingCrash) { await tx.crashReport.update({ where: { UUID_CR: existingCrash.UUID_CR }, data: { CrashCount_CR: { increment: 1 }, LastOccurred_CR: localTime(new Date()), UpdatedAt_CR: localTime(new Date()), AffectedUsers_CR: userId && userId !== existingCrash.UserId_CR ? { increment: 1 } : existingCrash.AffectedUsers_CR } }); } else { await tx.crashReport.create({ data: { AppId_CR: appId, AppVersion_CR: appVersion, BuildVersion_CR: buildVersion, CrashId_CR: crashId, SessionId_CR: sessionId, UserId_CR: userId, CrashType_CR: crashType, ExceptionName_CR: exceptionName, ExceptionReason_CR: exceptionReason, StackTrace_CR: stackTrace, ThreadName_CR: threadName, IsFatal_CR: isFatal || true, Severity_CR: severity || 'fatal', DeviceModel_CR: deviceModel, DeviceBrand_CR: deviceBrand, OSName_CR: osName || 'android', OSVersion_CR: osVersion, Architecture_CR: architecture, AvailableRam_CR: availableRam ? BigInt(availableRam) : null, TotalRam_CR: totalRam ? BigInt(totalRam) : null, AvailableDisk_CR: availableDisk ? BigInt(availableDisk) : null, TotalDisk_CR: totalDisk ? BigInt(totalDisk) : null, BatteryLevel_CR: batteryLevel, IsRooted_CR: isRooted || false, IsDebugger_CR: isDebugger || false, NetworkType_CR: networkType, Breadcrumbs_CR: breadcrumbs || [], CustomData_CR: customData || {}, Logs_CR: logs || [] } }); } if (sessionId) { const existingSession = await tx.crashSession.findUnique({ where: { SessionId_CS: sessionId } }); if (existingSession) { await tx.crashSession.update({ where: { SessionId_CS: sessionId }, data: { IsCrashed_CS: true, CrashCount_CS: { increment: 1 }, UpdatedAt_CS: localTime(new Date()) } }); } } const now = localTime(new Date()); const todayDate = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())); const updateData = { TotalCrashes_CA: { increment: 1 }, UpdatedAt_CA: localTime(new Date()), ...(isFatal ? { FatalCrashes_CA: { increment: 1 } } : { NonFatalCrashes_CA: { increment: 1 } }), ...(!existingCrash && { UniqueCrashes_CA: { increment: 1 } }), ...(userId && !existingCrash && { AffectedUsers_CA: { increment: 1 } }) }; const analyticsUpdated = await tx.crashAnalytics.updateMany({ where: { AppId_CA: appId, AppVersion_CA: appVersion, Date_CA: todayDate }, data: updateData }); if (analyticsUpdated.count === 0) { try { await tx.crashAnalytics.create({ data: { AppId_CA: appId, AppVersion_CA: appVersion, Date_CA: todayDate, TotalCrashes_CA: 1, FatalCrashes_CA: isFatal ? 1 : 0, NonFatalCrashes_CA: !isFatal ? 1 : 0, UniqueCrashes_CA: 1, AffectedUsers_CA: userId ? 1 : 0 } }); } catch (createError) { if (createError.code === 'P2002') { await tx.crashAnalytics.updateMany({ where: { AppId_CA: appId, AppVersion_CA: appVersion, Date_CA: todayDate }, data: updateData }); } else { throw createError; } } } }); return successResponse(res, "Crash report submitted successfully", { crashId }); } catch (error) { return errorResponse(res, "Failed to submit crash report"); } }; exports.startSession = async (req, res) => { try { const { sessionId, appId, appVersion, userId, deviceModel, osVersion, startedAt } = req.body; if (!sessionId || !appId || !appVersion) { return badRequestResponse(res, "Missing required fields: sessionId, appId, appVersion"); } const session = await prisma.crashSession.create({ data: { SessionId_CS: sessionId, AppId_CS: appId, AppVersion_CS: appVersion, UserId_CS: userId, DeviceModel_CS: deviceModel, OSVersion_CS: osVersion, StartedAt_CS: startedAt ? new Date(startedAt) : localTime(new Date()) } }); return successResponse(res, "Session started successfully", { sessionId }); } catch (error) { return errorResponse(res, "Failed to start session"); } }; exports.endSession = async (req, res) => { try { const { sessionId, endedAt, duration } = req.body; if (!sessionId) { return badRequestResponse(res, "Missing required field: sessionId"); } await prisma.$transaction(async (tx) => { const session = await tx.crashSession.findUnique({ where: { SessionId_CS: sessionId } }); if (!session) { throw new Error("Session not found"); } const updatedSession = await tx.crashSession.update({ where: { SessionId_CS: sessionId }, data: { EndedAt_CS: endedAt ? new Date(endedAt) : localTime(new Date()), Duration_CS: duration, UpdatedAt_CS: localTime(new Date()) } }); const now = localTime(new Date()); const todayDate = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())); // Get current analytics to calculate crash free rate const currentAnalytics = await tx.crashAnalytics.findUnique({ where: { AppId_CA_AppVersion_CA_Date_CA: { AppId_CA: session.AppId_CS, AppVersion_CA: session.AppVersion_CS, Date_CA: todayDate } } }); const newTotalSessions = (currentAnalytics?.TotalSessions_CA || 0) + 1; const newCrashedSessions = (currentAnalytics?.CrashedSessions_CA || 0) + (session.IsCrashed_CS ? 1 : 0); const crashFreeRate = newTotalSessions > 0 ? ((newTotalSessions - newCrashedSessions) / newTotalSessions * 100) : 100; const updateData = { TotalSessions_CA: { increment: 1 }, CrashFreeRate_CA: crashFreeRate, UpdatedAt_CA: localTime(new Date()), ...(session.IsCrashed_CS && { CrashedSessions_CA: { increment: 1 } }) }; const analyticsUpdated = await tx.crashAnalytics.updateMany({ where: { AppId_CA: session.AppId_CS, AppVersion_CA: session.AppVersion_CS, Date_CA: todayDate }, data: updateData }); if (analyticsUpdated.count === 0) { try { await tx.crashAnalytics.create({ data: { AppId_CA: session.AppId_CS, AppVersion_CA: session.AppVersion_CS, Date_CA: todayDate, TotalSessions_CA: 1, CrashedSessions_CA: session.IsCrashed_CS ? 1 : 0, CrashFreeRate_CA: crashFreeRate } }); } catch (createError) { if (createError.code === 'P2002') { await tx.crashAnalytics.updateMany({ where: { AppId_CA: session.AppId_CS, AppVersion_CA: session.AppVersion_CS, Date_CA: todayDate }, data: updateData }); } else { throw createError; } } } }); return successResponse(res, "Session ended successfully", { sessionId }); } catch (error) { return errorResponse(res, "Failed to end session"); } }; exports.getCrashReports = async (req, res) => { try { const { appId, appVersion, severity, status, page = 1, limit = 20, sortBy = 'LastOccurred_CR', sortOrder = 'desc' } = req.query; if (!appId) { return badRequestResponse(res, "Missing required field: appId"); } const skip = (parseInt(page) - 1) * parseInt(limit); const whereClause = { AppId_CR: appId, ...(appVersion && { AppVersion_CR: appVersion }), ...(severity && { Severity_CR: severity }), ...(status && { Status_CR: status }) }; const [crashes, totalCount] = await Promise.all([ prisma.crashReport.findMany({ where: whereClause, orderBy: { [sortBy]: sortOrder }, skip, take: parseInt(limit), select: { UUID_CR: true, CrashId_CR: true, CrashType_CR: true, ExceptionName_CR: true, Severity_CR: true, Status_CR: true, IsFatal_CR: true, CrashCount_CR: true, AffectedUsers_CR: true, FirstOccurred_CR: true, LastOccurred_CR: true, DeviceModel_CR: true, OSName_CR: true, OSVersion_CR: true, AppVersion_CR: true } }), prisma.crashReport.count({ where: whereClause }) ]); return successResponse(res, "Crash reports retrieved successfully", { crashes, pagination: { page: parseInt(page), limit: parseInt(limit), total: totalCount, totalPages: Math.ceil(totalCount / parseInt(limit)) } }); } catch (error) { return errorResponse(res, "Failed to retrieve crash reports"); } }; exports.getCrashDetails = async (req, res) => { try { const { crashId } = req.params; if (!crashId) { return badRequestResponse(res, "Missing required parameter: crashId"); } const crash = await prisma.crashReport.findUnique({ where: { CrashId_CR: crashId } }); if (!crash) { return badRequestResponse(res, "Crash report not found"); } return successResponse(res, "Crash details retrieved successfully", crash); } catch (error) { return errorResponse(res, "Failed to retrieve crash details"); } }; exports.updateCrashStatus = async (req, res) => { try { const { crashId } = req.params; const { status, resolvedBy } = req.body; if (!crashId || !status) { return badRequestResponse(res, "Missing required fields: crashId, status"); } const updateData = { Status_CR: status, UpdatedAt_CR: localTime(new Date()) }; if (status === 'resolved') { updateData.ResolvedAt_CR = localTime(new Date()); if (resolvedBy) updateData.ResolvedBy_CR = resolvedBy; } const updatedCrash = await prisma.crashReport.update({ where: { CrashId_CR: crashId }, data: updateData }); return successResponse(res, "Crash status updated successfully", updatedCrash); } catch (error) { return errorResponse(res, "Failed to update crash status"); } }; exports.getCrashAnalytics = async (req, res) => { try { const { appId, appVersion, startDate, endDate, groupBy = 'day' } = req.query; if (!appId) { return badRequestResponse(res, "Missing required field: appId"); } const whereClause = { AppId_CA: appId, ...(appVersion && { AppVersion_CA: appVersion }) }; if (startDate && endDate) { whereClause.Date_CA = { gte: localTime(new Date(startDate)), lte: localTime(new Date(endDate)) }; } const analytics = await prisma.crashAnalytics.findMany({ where: whereClause, orderBy: { Date_CA: 'desc' }, take: groupBy === 'day' ? 30 : 12 }); const summary = analytics.reduce((acc, day) => { acc.totalCrashes += day.TotalCrashes_CA; acc.totalSessions += day.TotalSessions_CA; acc.fatalCrashes += day.FatalCrashes_CA; acc.nonFatalCrashes += day.NonFatalCrashes_CA; acc.crashedSessions += day.CrashedSessions_CA; return acc; }, { totalCrashes: 0, totalSessions: 0, fatalCrashes: 0, nonFatalCrashes: 0, crashedSessions: 0 }); summary.crashFreeRate = summary.totalSessions > 0 ? ((summary.totalSessions - summary.crashedSessions) / summary.totalSessions * 100).toFixed(2) : 100; return successResponse(res, "Crash analytics retrieved successfully", { summary, daily: analytics }); } catch (error) { return errorResponse(res, "Failed to retrieve crash analytics"); } }; exports.getTopCrashes = async (req, res) => { try { const { appId, appVersion, limit = 10 } = req.query; if (!appId) { return badRequestResponse(res, "Missing required field: appId"); } const whereClause = { AppId_CR: appId, ...(appVersion && { AppVersion_CR: appVersion }) }; const topCrashes = await prisma.crashReport.findMany({ where: whereClause, orderBy: { CrashCount_CR: 'desc' }, take: parseInt(limit), select: { UUID_CR: true, CrashId_CR: true, CrashType_CR: true, ExceptionName_CR: true, Severity_CR: true, Status_CR: true, IsFatal_CR: true, CrashCount_CR: true, AffectedUsers_CR: true, LastOccurred_CR: true, DeviceModel_CR: true, OSName_CR: true, OSVersion_CR: true } }); return successResponse(res, "Top crashes retrieved successfully", topCrashes); } catch (error) { return errorResponse(res, "Failed to retrieve top crashes"); } }; exports.getCrashTrends = async (req, res) => { try { const { appId, appVersion, startDate, endDate, interval = 'day' } = req.query; if (!appId) { return badRequestResponse(res, "Missing required field: appId"); } const whereClause = { AppId_CA: appId, ...(appVersion && { AppVersion_CA: appVersion }) }; if (startDate && endDate) { whereClause.Date_CA = { gte: localTime(new Date(startDate)), lte: localTime(new Date(endDate)) }; } else { const thirtyDaysAgo = localTime(new Date()); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); whereClause.Date_CA = { gte: thirtyDaysAgo }; } const analytics = await prisma.crashAnalytics.findMany({ where: whereClause, orderBy: { Date_CA: 'asc' }, select: { Date_CA: true, TotalCrashes_CA: true, FatalCrashes_CA: true, NonFatalCrashes_CA: true, CrashFreeRate_CA: true, TotalSessions_CA: true } }); const chartData = analytics.map(day => ({ date: day.Date_CA.toISOString().split('T')[0], totalCrashes: day.TotalCrashes_CA, fatalCrashes: day.FatalCrashes_CA, nonFatalCrashes: day.NonFatalCrashes_CA, crashFreeRate: day.CrashFreeRate_CA || 0, totalSessions: day.TotalSessions_CA })); return successResponse(res, "Crash trends retrieved successfully", chartData); } catch (error) { return errorResponse(res, "Failed to retrieve crash trends"); } }; exports.getCrashByDevice = async (req, res) => { try { const { appId, appVersion, days = 30 } = req.query; if (!appId) { return badRequestResponse(res, "Missing required field: appId"); } const daysAgo = localTime(new Date()); daysAgo.setDate(daysAgo.getDate() - parseInt(days)); const whereClause = { AppId_CR: appId, CreatedAt_CR: { gte: daysAgo }, ...(appVersion && { AppVersion_CR: appVersion }) }; const crashes = await prisma.crashReport.groupBy({ by: ['DeviceModel_CR'], where: whereClause, _sum: { CrashCount_CR: true }, _count: { UUID_CR: true }, orderBy: { _sum: { CrashCount_CR: 'desc' } }, take: 10 }); const chartData = crashes.map(item => ({ device: item.DeviceModel_CR || 'Unknown', crashCount: item._sum.CrashCount_CR || 0, uniqueCrashes: item._count.UUID_CR })); const totalCrashes = chartData.reduce((sum, item) => sum + item.crashCount, 0); const chartDataWithPercentage = chartData.map(item => ({ ...item, percentage: ((item.crashCount / totalCrashes) * 100).toFixed(2) })); return successResponse(res, "Crash distribution by device retrieved successfully", chartDataWithPercentage); } catch (error) { return errorResponse(res, "Failed to retrieve crash distribution by device"); } }; exports.getCrashByOSVersion = async (req, res) => { try { const { appId, appVersion, days = 30 } = req.query; if (!appId) { return badRequestResponse(res, "Missing required field: appId"); } const daysAgo = localTime(new Date()); daysAgo.setDate(daysAgo.getDate() - parseInt(days)); const whereClause = { AppId_CR: appId, CreatedAt_CR: { gte: daysAgo }, ...(appVersion && { AppVersion_CR: appVersion }) }; const crashes = await prisma.crashReport.groupBy({ by: ['OSVersion_CR'], where: whereClause, _sum: { CrashCount_CR: true }, _count: { UUID_CR: true }, orderBy: { _sum: { CrashCount_CR: 'desc' } } }); const chartData = crashes.map(item => ({ osVersion: item.OSVersion_CR || 'Unknown', crashCount: item._sum.CrashCount_CR || 0, uniqueCrashes: item._count.UUID_CR })); return successResponse(res, "Crash distribution by OS version retrieved successfully", chartData); } catch (error) { return errorResponse(res, "Failed to retrieve crash distribution by OS version"); } }; exports.getCrashByExceptionType = async (req, res) => { try { const { appId, appVersion, days = 30, limit = 15 } = req.query; if (!appId) { return badRequestResponse(res, "Missing required field: appId"); } const daysAgo = localTime(new Date()); daysAgo.setDate(daysAgo.getDate() - parseInt(days)); const whereClause = { AppId_CR: appId, CreatedAt_CR: { gte: daysAgo }, ...(appVersion && { AppVersion_CR: appVersion }) }; const crashes = await prisma.crashReport.groupBy({ by: ['ExceptionName_CR'], where: whereClause, _sum: { CrashCount_CR: true, AffectedUsers_CR: true }, _count: { UUID_CR: true }, orderBy: { _sum: { CrashCount_CR: 'desc' } }, take: parseInt(limit) }); const chartData = crashes.map(item => ({ exceptionType: item.ExceptionName_CR || 'Unknown', crashCount: item._sum.CrashCount_CR || 0, affectedUsers: item._sum.AffectedUsers_CR || 0, uniqueInstances: item._count.UUID_CR })); return successResponse(res, "Crash distribution by exception type retrieved successfully", chartData); } catch (error) { return errorResponse(res, "Failed to retrieve crash distribution by exception type"); } }; exports.getCrashBySeverity = async (req, res) => { try { const { appId, appVersion, days = 30 } = req.query; if (!appId) { return badRequestResponse(res, "Missing required field: appId"); } const daysAgo = localTime(new Date()); daysAgo.setDate(daysAgo.getDate() - parseInt(days)); const whereClause = { AppId_CR: appId, CreatedAt_CR: { gte: daysAgo }, ...(appVersion && { AppVersion_CR: appVersion }) }; const crashes = await prisma.crashReport.groupBy({ by: ['Severity_CR'], where: whereClause, _sum: { CrashCount_CR: true }, _count: { UUID_CR: true } }); const chartData = crashes.map(item => ({ severity: item.Severity_CR, crashCount: item._sum.CrashCount_CR || 0, uniqueCrashes: item._count.UUID_CR })); const totalCrashes = chartData.reduce((sum, item) => sum + item.crashCount, 0); const chartDataWithPercentage = chartData.map(item => ({ ...item, percentage: ((item.crashCount / totalCrashes) * 100).toFixed(2), color: getSeverityColor(item.severity) })); return successResponse(res, "Crash distribution by severity retrieved successfully", chartDataWithPercentage); } catch (error) { return errorResponse(res, "Failed to retrieve crash distribution by severity"); } }; exports.getCrashByAppVersion = async (req, res) => { try { const { appId, days = 30 } = req.query; if (!appId) { return badRequestResponse(res, "Missing required field: appId"); } const daysAgo = localTime(new Date()); daysAgo.setDate(daysAgo.getDate() - parseInt(days)); const whereClause = { AppId_CR: appId, CreatedAt_CR: { gte: daysAgo } }; const crashes = await prisma.crashReport.groupBy({ by: ['AppVersion_CR', 'IsFatal_CR'], where: whereClause, _sum: { CrashCount_CR: true }, _count: { UUID_CR: true } }); const versionMap = {}; crashes.forEach(item => { const version = item.AppVersion_CR; if (!versionMap[version]) { versionMap[version] = { version, fatalCrashes: 0, nonFatalCrashes: 0, totalCrashes: 0, uniqueCrashes: 0 }; } const crashCount = item._sum.CrashCount_CR || 0; versionMap[version].totalCrashes += crashCount; versionMap[version].uniqueCrashes += item._count.UUID_CR; if (item.IsFatal_CR) { versionMap[version].fatalCrashes += crashCount; } else { versionMap[version].nonFatalCrashes += crashCount; } }); const chartData = Object.values(versionMap).sort((a, b) => b.totalCrashes - a.totalCrashes); return successResponse(res, "Crash distribution by app version retrieved successfully", chartData); } catch (error) { return errorResponse(res, "Failed to retrieve crash distribution by app version"); } }; exports.getCrashHeatmap = async (req, res) => { try { const { appId, appVersion, year, month } = req.query; if (!appId) { return badRequestResponse(res, "Missing required field: appId"); } const currentDate = localTime(new Date()); const targetYear = year ? parseInt(year) : currentDate.getFullYear(); const targetMonth = month ? parseInt(month) : currentDate.getMonth() + 1; const startDate = localTime(new Date(targetYear, targetMonth - 1, 1)); const endDate = localTime(new Date(targetYear, targetMonth, 0)); const whereClause = { AppId_CA: appId, Date_CA: { gte: startDate, lte: endDate }, ...(appVersion && { AppVersion_CA: appVersion }) }; const analytics = await prisma.crashAnalytics.findMany({ where: whereClause, orderBy: { Date_CA: 'asc' }, select: { Date_CA: true, TotalCrashes_CA: true, FatalCrashes_CA: true, CrashFreeRate_CA: true } }); const chartData = analytics.map(day => ({ date: day.Date_CA.toISOString().split('T')[0], crashCount: day.TotalCrashes_CA, fatalCount: day.FatalCrashes_CA, crashFreeRate: day.CrashFreeRate_CA || 0, intensity: calculateIntensity(day.TotalCrashes_CA) })); return successResponse(res, "Crash heatmap data retrieved successfully", { year: targetYear, month: targetMonth, data: chartData }); } catch (error) { return errorResponse(res, "Failed to retrieve crash heatmap data"); } }; exports.getCrashByHour = async (req, res) => { try { const { appId, appVersion, days = 7 } = req.query; if (!appId) { return badRequestResponse(res, "Missing required field: appId"); } const daysAgo = localTime(new Date()); daysAgo.setDate(daysAgo.getDate() - parseInt(days)); const whereClause = { AppId_CR: appId, CreatedAt_CR: { gte: daysAgo }, ...(appVersion && { AppVersion_CR: appVersion }) }; const crashes = await prisma.crashReport.findMany({ where: whereClause, select: { CreatedAt_CR: true, IsFatal_CR: true, CrashCount_CR: true } }); const hourlyData = Array(24).fill(0).map((_, hour) => ({ hour, fatalCrashes: 0, nonFatalCrashes: 0, totalCrashes: 0 })); crashes.forEach(crash => { const hour = new Date(crash.CreatedAt_CR).getHours(); hourlyData[hour].totalCrashes += crash.CrashCount_CR; if (crash.IsFatal_CR) { hourlyData[hour].fatalCrashes += crash.CrashCount_CR; } else { hourlyData[hour].nonFatalCrashes += crash.CrashCount_CR; } }); return successResponse(res, "Hourly crash distribution retrieved successfully", hourlyData); } catch (error) { return errorResponse(res, "Failed to retrieve hourly crash distribution"); } }; exports.getCrashOverview = async (req, res) => { try { const { appId, appVersion, days = 30 } = req.query; if (!appId) { return badRequestResponse(res, "Missing required field: appId"); } const daysAgo = localTime(new Date()); daysAgo.setDate(daysAgo.getDate() - parseInt(days)); const whereClause = { AppId_CR: appId, CreatedAt_CR: { gte: daysAgo }, ...(appVersion && { AppVersion_CR: appVersion }) }; const [ totalCrashes, fatalCrashes, uniqueCrashTypesData, affectedDevicesData, recentCrashes ] = await Promise.all([ prisma.crashReport.aggregate({ where: whereClause, _sum: { CrashCount_CR: true } }), prisma.crashReport.aggregate({ where: { ...whereClause, IsFatal_CR: true }, _sum: { CrashCount_CR: true } }), prisma.crashReport.findMany({ where: whereClause, distinct: ['ExceptionName_CR'], select: { ExceptionName_CR: true } }), prisma.crashReport.findMany({ where: whereClause, distinct: ['DeviceModel_CR'], select: { DeviceModel_CR: true } }), prisma.crashReport.findMany({ where: whereClause, orderBy: { LastOccurred_CR: 'desc' }, take: 5, select: { CrashId_CR: true, ExceptionName_CR: true, CrashCount_CR: true, LastOccurred_CR: true, Severity_CR: true } }) ]); const uniqueCrashTypes = uniqueCrashTypesData.length; const affectedDevices = affectedDevicesData.length; const sessionStats = await prisma.crashSession.aggregate({ where: { AppId_CS: appId, StartedAt_CS: { gte: daysAgo }, ...(appVersion && { AppVersion_CS: appVersion }) }, _count: { SessionId_CS: true }, _sum: { CrashCount_CS: true } }); const totalSessions = sessionStats._count.SessionId_CS || 0; const crashedSessions = sessionStats._sum.CrashCount_CS || 0; const crashFreeRate = totalSessions > 0 ? ((totalSessions - crashedSessions) / totalSessions * 100).toFixed(2) : 100; const [resolvedCount, unresolvedCount] = await Promise.all([ prisma.crashReport.count({ where: { ...whereClause, Status_CR: 'resolved' } }), prisma.crashReport.count({ where: { ...whereClause, Status_CR: { in: ['new', 'acknowledged'] } } }) ]); const overview = { totalCrashes: totalCrashes._sum.CrashCount_CR || 0, fatalCrashes: fatalCrashes._sum.CrashCount_CR || 0, nonFatalCrashes: (totalCrashes._sum.CrashCount_CR || 0) - (fatalCrashes._sum.CrashCount_CR || 0), uniqueCrashTypes, affectedDevices, crashFreeRate: parseFloat(crashFreeRate), totalSessions, crashedSessions, resolvedCrashes: resolvedCount, unresolvedCrashes: unresolvedCount, recentCrashes }; return successResponse(res, "Crash overview retrieved successfully", overview); } catch (error) { return errorResponse(res, "Failed to retrieve crash overview"); } }; exports.getAffectedUsersTimeline = async (req, res) => { try { const { appId, appVersion, startDate, endDate } = req.query; if (!appId) { return badRequestResponse(res, "Missing required field: appId"); } const whereClause = { AppId_CA: appId, ...(appVersion && { AppVersion_CA: appVersion }) }; if (startDate && endDate) { whereClause.Date_CA = { gte: localTime(new Date(startDate)), lte: localTime(new Date(endDate)) }; } else { const thirtyDaysAgo = localTime(new Date()); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); whereClause.Date_CA = { gte: thirtyDaysAgo }; } const analytics = await prisma.crashAnalytics.findMany({ where: whereClause, orderBy: { Date_CA: 'asc' }, select: { Date_CA: true, AffectedUsers_CA: true, TotalSessions_CA: true } }); const chartData = analytics.map(day => ({ date: day.Date_CA.toISOString().split('T')[0], affectedUsers: day.AffectedUsers_CA, totalSessions: day.TotalSessions_CA, affectedRate: day.TotalSessions_CA > 0 ? ((day.AffectedUsers_CA / day.TotalSessions_CA) * 100).toFixed(2) : 0 })); return successResponse(res, "Affected users timeline retrieved successfully", chartData); } catch (error) { return errorResponse(res, "Failed to retrieve affected users timeline"); } }; function getSeverityColor(severity) { const colors = { fatal: '#DC2626', error: '#EA580C', warning: '#F59E0B', info: '#3B82F6' }; return colors[severity] || '#6B7280'; } function calculateIntensity(crashCount) { if (crashCount === 0) return 0; if (crashCount <= 5) return 1; if (crashCount <= 10) return 2; if (crashCount <= 20) return 3; if (crashCount <= 50) return 4; return 5; }