@crashsense/core 1.0.7 → 1.1.1

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