@crashsense/core 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -30,7 +30,12 @@ function resolveConfig(userConfig) {
30
30
  preCrashMemoryThreshold: userConfig.preCrashMemoryThreshold ?? 0.8,
31
31
  piiScrubbing: userConfig.piiScrubbing ?? true,
32
32
  debug: userConfig.debug ?? false,
33
- onCrash: userConfig.onCrash ?? null
33
+ onCrash: userConfig.onCrash ?? null,
34
+ enableOOMRecovery: userConfig.enableOOMRecovery ?? false,
35
+ checkpointInterval: userConfig.checkpointInterval ?? 1e4,
36
+ oomRecoveryThreshold: userConfig.oomRecoveryThreshold ?? 0.3,
37
+ flushEndpoint: userConfig.flushEndpoint ?? null,
38
+ onOOMRecovery: userConfig.onOOMRecovery ?? null
34
39
  };
35
40
  }
36
41
 
@@ -968,6 +973,373 @@ function classifyCrash(event) {
968
973
  };
969
974
  }
970
975
 
976
+ // src/checkpoint-manager.ts
977
+ var CHECKPOINT_KEY = "__crashsense_checkpoint";
978
+ var CHECKPOINT_VERSION = 1;
979
+ function createCheckpointManager(bus, config, sessionId, deviceInfo, getSystemState, getBreadcrumbs) {
980
+ let intervalId = null;
981
+ let checkpointCount = 0;
982
+ const preCrashWarnings = [];
983
+ let lastMemoryTrend = null;
984
+ function hasSessionStorage() {
985
+ try {
986
+ return typeof sessionStorage !== "undefined" && sessionStorage !== null;
987
+ } catch {
988
+ return false;
989
+ }
990
+ }
991
+ function writeCheckpoint() {
992
+ if (!hasSessionStorage()) return;
993
+ checkpointCount++;
994
+ const systemState = getSystemState();
995
+ if (systemState.memory?.trend) {
996
+ lastMemoryTrend = systemState.memory.trend;
997
+ }
998
+ const checkpoint = {
999
+ version: CHECKPOINT_VERSION,
1000
+ timestamp: Date.now(),
1001
+ sessionId,
1002
+ appId: config.appId,
1003
+ url: typeof location !== "undefined" ? location.href : "",
1004
+ breadcrumbs: getBreadcrumbs().slice(-20),
1005
+ systemState,
1006
+ device: deviceInfo,
1007
+ preCrashWarnings: preCrashWarnings.slice(-5),
1008
+ memoryTrend: lastMemoryTrend,
1009
+ checkpointCount
1010
+ };
1011
+ try {
1012
+ sessionStorage.setItem(CHECKPOINT_KEY, JSON.stringify(checkpoint));
1013
+ bus.emit("checkpoint_written", { timestamp: checkpoint.timestamp, sessionId });
1014
+ } catch {
1015
+ try {
1016
+ const minimal = {
1017
+ ...checkpoint,
1018
+ breadcrumbs: checkpoint.breadcrumbs.slice(-5),
1019
+ systemState: { memory: systemState.memory }
1020
+ };
1021
+ sessionStorage.setItem(CHECKPOINT_KEY, JSON.stringify(minimal));
1022
+ } catch {
1023
+ }
1024
+ }
1025
+ }
1026
+ bus.on("pre_crash_warning", (data) => {
1027
+ preCrashWarnings.push({
1028
+ level: data.level,
1029
+ reason: data.reason,
1030
+ timestamp: data.timestamp
1031
+ });
1032
+ while (preCrashWarnings.length > 10) {
1033
+ preCrashWarnings.shift();
1034
+ }
1035
+ writeCheckpoint();
1036
+ });
1037
+ bus.on("crash_detected", () => {
1038
+ writeCheckpoint();
1039
+ });
1040
+ return {
1041
+ start() {
1042
+ if (typeof window === "undefined") return;
1043
+ if (!hasSessionStorage()) return;
1044
+ writeCheckpoint();
1045
+ intervalId = setInterval(writeCheckpoint, config.checkpointInterval);
1046
+ },
1047
+ stop() {
1048
+ if (intervalId !== null) {
1049
+ clearInterval(intervalId);
1050
+ intervalId = null;
1051
+ }
1052
+ },
1053
+ /** Force an immediate checkpoint write (used by lifecycle flush) */
1054
+ flush() {
1055
+ writeCheckpoint();
1056
+ },
1057
+ /** Read and remove the stored checkpoint (used by OOM recovery on next load) */
1058
+ static: {
1059
+ readCheckpoint() {
1060
+ try {
1061
+ if (typeof sessionStorage === "undefined") return null;
1062
+ const raw = sessionStorage.getItem(CHECKPOINT_KEY);
1063
+ if (!raw) return null;
1064
+ const data = JSON.parse(raw);
1065
+ if (data.version !== CHECKPOINT_VERSION) return null;
1066
+ return data;
1067
+ } catch {
1068
+ return null;
1069
+ }
1070
+ },
1071
+ clearCheckpoint() {
1072
+ try {
1073
+ if (typeof sessionStorage !== "undefined") {
1074
+ sessionStorage.removeItem(CHECKPOINT_KEY);
1075
+ }
1076
+ } catch {
1077
+ }
1078
+ }
1079
+ },
1080
+ /** Clear checkpoint on clean shutdown (destroy) */
1081
+ clearOnDestroy() {
1082
+ try {
1083
+ if (hasSessionStorage()) {
1084
+ sessionStorage.removeItem(CHECKPOINT_KEY);
1085
+ }
1086
+ } catch {
1087
+ }
1088
+ }
1089
+ };
1090
+ }
1091
+ function readCheckpoint() {
1092
+ try {
1093
+ if (typeof sessionStorage === "undefined") return null;
1094
+ const raw = sessionStorage.getItem(CHECKPOINT_KEY);
1095
+ if (!raw) return null;
1096
+ const data = JSON.parse(raw);
1097
+ if (data.version !== CHECKPOINT_VERSION) return null;
1098
+ return data;
1099
+ } catch {
1100
+ return null;
1101
+ }
1102
+ }
1103
+ function clearCheckpoint() {
1104
+ try {
1105
+ if (typeof sessionStorage !== "undefined") {
1106
+ sessionStorage.removeItem(CHECKPOINT_KEY);
1107
+ }
1108
+ } catch {
1109
+ }
1110
+ }
1111
+ function getNavigationType() {
1112
+ try {
1113
+ if (typeof performance === "undefined") return "unknown";
1114
+ const entries = performance.getEntriesByType("navigation");
1115
+ if (entries.length > 0) {
1116
+ return entries[0].type || "unknown";
1117
+ }
1118
+ } catch {
1119
+ }
1120
+ return "unknown";
1121
+ }
1122
+ function getWasDiscarded() {
1123
+ try {
1124
+ const doc = document;
1125
+ if ("wasDiscarded" in doc) {
1126
+ return doc.wasDiscarded;
1127
+ }
1128
+ } catch {
1129
+ }
1130
+ return void 0;
1131
+ }
1132
+ function detectOOMRecovery(bus, config, currentSessionId) {
1133
+ if (typeof window === "undefined") return null;
1134
+ const checkpoint = readCheckpoint();
1135
+ if (!checkpoint) return null;
1136
+ clearCheckpoint();
1137
+ const now = Date.now();
1138
+ const timeSinceCheckpoint = now - checkpoint.timestamp;
1139
+ const MAX_OOM_AGE = 5 * 60 * 1e3;
1140
+ if (timeSinceCheckpoint > MAX_OOM_AGE) return null;
1141
+ if (checkpoint.appId !== config.appId) return null;
1142
+ const wasDiscarded = getWasDiscarded();
1143
+ const navigationType = getNavigationType();
1144
+ const deviceInfo = collectDeviceInfo();
1145
+ const signals = [];
1146
+ let probability = 0;
1147
+ if (wasDiscarded === true) {
1148
+ probability += 0.45;
1149
+ signals.push({
1150
+ signal: "document_was_discarded",
1151
+ weight: 0.45,
1152
+ evidence: "document.wasDiscarded is true \u2014 OS confirmed tab discard"
1153
+ });
1154
+ }
1155
+ if (navigationType === "reload" && timeSinceCheckpoint < 3e4) {
1156
+ probability += 0.25;
1157
+ signals.push({
1158
+ signal: "recent_reload",
1159
+ weight: 0.25,
1160
+ evidence: `Page reloaded ${Math.round(timeSinceCheckpoint / 1e3)}s after last checkpoint`
1161
+ });
1162
+ } else if (navigationType === "reload" && timeSinceCheckpoint < 6e4) {
1163
+ probability += 0.15;
1164
+ signals.push({
1165
+ signal: "moderate_reload",
1166
+ weight: 0.15,
1167
+ evidence: `Page reloaded ${Math.round(timeSinceCheckpoint / 1e3)}s after last checkpoint`
1168
+ });
1169
+ }
1170
+ if (checkpoint.memoryTrend === "growing") {
1171
+ probability += 0.1;
1172
+ signals.push({
1173
+ signal: "memory_growing_trend",
1174
+ weight: 0.1,
1175
+ evidence: "Memory was in growing trend before tab died"
1176
+ });
1177
+ } else if (checkpoint.memoryTrend === "spike") {
1178
+ probability += 0.08;
1179
+ signals.push({
1180
+ signal: "memory_spike",
1181
+ weight: 0.08,
1182
+ evidence: "Memory was spiking before tab died"
1183
+ });
1184
+ }
1185
+ if (checkpoint.preCrashWarnings.length > 0) {
1186
+ const highestLevel = checkpoint.preCrashWarnings.reduce((max, w) => {
1187
+ const severity = { elevated: 1, critical: 2, imminent: 3 };
1188
+ return (severity[w.level] || 0) > (severity[max.level] || 0) ? w : max;
1189
+ }, checkpoint.preCrashWarnings[0]);
1190
+ if (highestLevel.level === "imminent") {
1191
+ probability += 0.15;
1192
+ signals.push({
1193
+ signal: "pre_crash_warning_imminent",
1194
+ weight: 0.15,
1195
+ evidence: `Pre-crash warning at IMMINENT level: ${highestLevel.reason}`
1196
+ });
1197
+ } else if (highestLevel.level === "critical") {
1198
+ probability += 0.1;
1199
+ signals.push({
1200
+ signal: "pre_crash_warning_critical",
1201
+ weight: 0.1,
1202
+ evidence: `Pre-crash warning at CRITICAL level: ${highestLevel.reason}`
1203
+ });
1204
+ } else {
1205
+ probability += 0.05;
1206
+ signals.push({
1207
+ signal: "pre_crash_warning_elevated",
1208
+ weight: 0.05,
1209
+ evidence: `Pre-crash warning at ELEVATED level: ${highestLevel.reason}`
1210
+ });
1211
+ }
1212
+ }
1213
+ const memUtil = checkpoint.systemState?.memory?.utilizationPercent;
1214
+ if (memUtil !== void 0 && memUtil !== null && memUtil > 85) {
1215
+ probability += 0.1;
1216
+ signals.push({
1217
+ signal: "high_memory_at_checkpoint",
1218
+ weight: 0.1,
1219
+ evidence: `Memory was at ${memUtil}% utilization at last checkpoint`
1220
+ });
1221
+ }
1222
+ if (checkpoint.device.touchSupport && checkpoint.device.deviceMemory !== null && checkpoint.device.deviceMemory <= 4) {
1223
+ probability += 0.05;
1224
+ signals.push({
1225
+ signal: "mobile_low_memory_device",
1226
+ weight: 0.05,
1227
+ evidence: `Touch device with ${checkpoint.device.deviceMemory}GB RAM \u2014 higher OOM risk`
1228
+ });
1229
+ }
1230
+ probability = Math.min(probability, 1);
1231
+ if (probability < config.oomRecoveryThreshold) return null;
1232
+ const report = {
1233
+ id: generateId(),
1234
+ type: "oom_recovery",
1235
+ timestamp: now,
1236
+ probability,
1237
+ sessionId: currentSessionId,
1238
+ previousSessionId: checkpoint.sessionId,
1239
+ timeSinceLastCheckpoint: timeSinceCheckpoint,
1240
+ wasDiscarded,
1241
+ navigationType,
1242
+ lastCheckpoint: checkpoint,
1243
+ device: deviceInfo,
1244
+ signals
1245
+ };
1246
+ bus.emit("oom_recovery", { report });
1247
+ if (config.onOOMRecovery) {
1248
+ try {
1249
+ config.onOOMRecovery(report);
1250
+ } catch {
1251
+ }
1252
+ }
1253
+ if (config.debug) {
1254
+ logOOMRecovery(report);
1255
+ }
1256
+ return report;
1257
+ }
1258
+ function logOOMRecovery(report) {
1259
+ if (typeof console === "undefined") return;
1260
+ const header = `[CrashSense] OOM Recovery Detected (${(report.probability * 100).toFixed(0)}% confidence)`;
1261
+ const details = [
1262
+ `Previous session: ${report.previousSessionId}`,
1263
+ `Time since last checkpoint: ${Math.round(report.timeSinceLastCheckpoint / 1e3)}s`,
1264
+ `Navigation type: ${report.navigationType}`,
1265
+ `document.wasDiscarded: ${report.wasDiscarded}`,
1266
+ `Last URL: ${report.lastCheckpoint.url}`,
1267
+ `Memory trend: ${report.lastCheckpoint.memoryTrend || "unknown"}`,
1268
+ `Pre-crash warnings: ${report.lastCheckpoint.preCrashWarnings.length}`,
1269
+ `Breadcrumbs recovered: ${report.lastCheckpoint.breadcrumbs.length}`
1270
+ ];
1271
+ console.groupCollapsed?.(header) ?? console.log(header);
1272
+ for (const d of details) console.log(d);
1273
+ if (report.signals.length > 0) {
1274
+ console.log("Signals:");
1275
+ for (const s of report.signals) {
1276
+ console.log(` - ${s.signal} (${(s.weight * 100).toFixed(0)}%): ${s.evidence}`);
1277
+ }
1278
+ }
1279
+ console.groupEnd?.();
1280
+ }
1281
+
1282
+ // src/lifecycle-flush.ts
1283
+ function createLifecycleFlush(bus, config, sessionId, getSystemState, getBreadcrumbs, flushCheckpoint) {
1284
+ let installed = false;
1285
+ function buildFlushPayload(reason) {
1286
+ return JSON.stringify({
1287
+ type: "lifecycle_flush",
1288
+ reason,
1289
+ timestamp: Date.now(),
1290
+ sessionId,
1291
+ appId: config.appId,
1292
+ url: typeof location !== "undefined" ? location.href : "",
1293
+ breadcrumbs: getBreadcrumbs().slice(-10),
1294
+ systemState: getSystemState()
1295
+ });
1296
+ }
1297
+ function flush(reason) {
1298
+ flushCheckpoint();
1299
+ if (config.flushEndpoint && typeof navigator !== "undefined" && navigator.sendBeacon) {
1300
+ try {
1301
+ const payload = buildFlushPayload(reason);
1302
+ navigator.sendBeacon(config.flushEndpoint, payload);
1303
+ } catch {
1304
+ }
1305
+ }
1306
+ bus.emit("lifecycle_flush", { reason, timestamp: Date.now() });
1307
+ }
1308
+ function handleVisibilityChange() {
1309
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
1310
+ flush("visibilitychange_hidden");
1311
+ }
1312
+ }
1313
+ function handlePageHide() {
1314
+ flush("pagehide");
1315
+ }
1316
+ function handleFreeze() {
1317
+ flush("freeze");
1318
+ }
1319
+ return {
1320
+ install() {
1321
+ if (typeof window === "undefined") return;
1322
+ if (installed) return;
1323
+ installed = true;
1324
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1325
+ window.addEventListener("pagehide", handlePageHide, { capture: true });
1326
+ if ("onfreeze" in document) {
1327
+ document.addEventListener("freeze", handleFreeze);
1328
+ }
1329
+ },
1330
+ uninstall() {
1331
+ if (typeof window === "undefined") return;
1332
+ if (!installed) return;
1333
+ installed = false;
1334
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1335
+ window.removeEventListener("pagehide", handlePageHide, { capture: true });
1336
+ if ("onfreeze" in document) {
1337
+ document.removeEventListener("freeze", handleFreeze);
1338
+ }
1339
+ }
1340
+ };
1341
+ }
1342
+
971
1343
  // src/crashsense.ts
972
1344
  function createCrashSense(userConfig) {
973
1345
  const config = resolveConfig(userConfig);
@@ -985,6 +1357,34 @@ function createCrashSense(userConfig) {
985
1357
  const breadcrumbTracker = createBreadcrumbTracker(bus);
986
1358
  const iframeTracker = createIframeTracker(bus);
987
1359
  const preCrashWarning = createPreCrashWarning(bus, config, memoryMonitor, iframeTracker);
1360
+ const checkpointManager = createCheckpointManager(
1361
+ bus,
1362
+ config,
1363
+ sessionId,
1364
+ deviceInfo,
1365
+ () => ({
1366
+ memory: memoryMonitor.getSnapshot(),
1367
+ cpu: eventLoopMonitor.getCpuSnapshot(),
1368
+ eventLoop: eventLoopMonitor.getEventLoopSnapshot(),
1369
+ network: networkMonitor.getSnapshot(),
1370
+ iframe: config.enableIframeTracking ? iframeTracker.getSnapshot() : void 0
1371
+ }),
1372
+ () => breadcrumbTracker.getBreadcrumbs()
1373
+ );
1374
+ const lifecycleFlush = createLifecycleFlush(
1375
+ bus,
1376
+ config,
1377
+ sessionId,
1378
+ () => ({
1379
+ memory: memoryMonitor.getSnapshot(),
1380
+ cpu: eventLoopMonitor.getCpuSnapshot(),
1381
+ eventLoop: eventLoopMonitor.getEventLoopSnapshot(),
1382
+ network: networkMonitor.getSnapshot(),
1383
+ iframe: config.enableIframeTracking ? iframeTracker.getSnapshot() : void 0
1384
+ }),
1385
+ () => breadcrumbTracker.getBreadcrumbs(),
1386
+ () => checkpointManager.flush()
1387
+ );
988
1388
  function buildRawEvent(error, source) {
989
1389
  const stack = error.stack ? parseStackTrace(error.stack) : [];
990
1390
  return {
@@ -1133,6 +1533,9 @@ function createCrashSense(userConfig) {
1133
1533
  data: { level: data.level, memoryUtilization: data.memoryUtilization, iframeCount: data.iframeCount }
1134
1534
  });
1135
1535
  });
1536
+ if (config.enableOOMRecovery) {
1537
+ detectOOMRecovery(bus, config, sessionId);
1538
+ }
1136
1539
  errorInterceptor.install();
1137
1540
  breadcrumbTracker.install();
1138
1541
  if (config.enableMemoryMonitoring) memoryMonitor.start();
@@ -1140,6 +1543,10 @@ function createCrashSense(userConfig) {
1140
1543
  if (config.enableNetworkMonitoring) networkMonitor.start();
1141
1544
  if (config.enableIframeTracking) iframeTracker.start();
1142
1545
  if (config.enablePreCrashWarning) preCrashWarning.start();
1546
+ if (config.enableOOMRecovery) {
1547
+ checkpointManager.start();
1548
+ lifecycleFlush.install();
1549
+ }
1143
1550
  const core = {
1144
1551
  get config() {
1145
1552
  return config;
@@ -1197,6 +1604,9 @@ function createCrashSense(userConfig) {
1197
1604
  networkMonitor.stop();
1198
1605
  iframeTracker.stop();
1199
1606
  preCrashWarning.stop();
1607
+ checkpointManager.stop();
1608
+ checkpointManager.clearOnDestroy();
1609
+ lifecycleFlush.uninstall();
1200
1610
  for (const plugin of plugins) {
1201
1611
  plugin.teardown();
1202
1612
  }