csa-backend-test/app/controllers/crash.controller.js

1106 lines
31 KiB
JavaScript

// 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;
}