1106 lines
31 KiB
JavaScript
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;
|
|
} |