@koderlabs/tasks-sdk-rn-native 0.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/LICENSE +179 -0
- package/README.md +9 -0
- package/TasksSdkRnNative.podspec +32 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/CMakeLists.txt +22 -0
- package/android/src/main/cpp/tasks_native_crash.cpp +174 -0
- package/android/src/main/java/expo/modules/tasksnative/TasksSdkRnNativeModule.kt +188 -0
- package/dist/index.cjs +132 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +83 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +112 -0
- package/dist/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/TasksSdkRnNativeModule.swift +167 -0
- package/package.json +85 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
enableNativeCapture: () => enableNativeCapture,
|
|
25
|
+
generateTestNativeReport: () => generateTestNativeReport,
|
|
26
|
+
getPendingNativeCrash: () => getPendingNativeCrash,
|
|
27
|
+
purgePendingNativeCrash: () => purgePendingNativeCrash
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
function loadNative(onError) {
|
|
31
|
+
try {
|
|
32
|
+
const core = (typeof require !== "undefined" ? require : null)?.("expo-modules-core");
|
|
33
|
+
if (!core?.requireOptionalNativeModule) {
|
|
34
|
+
onError?.({
|
|
35
|
+
stage: "require-core",
|
|
36
|
+
message: "expo-modules-core not available"
|
|
37
|
+
});
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const mod = core.requireOptionalNativeModule("TasksSdkRnNative");
|
|
41
|
+
if (!mod) onError?.({
|
|
42
|
+
stage: "require-native",
|
|
43
|
+
message: "TasksSdkRnNative native module not present"
|
|
44
|
+
});
|
|
45
|
+
return mod;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
onError?.({
|
|
48
|
+
stage: "require-core",
|
|
49
|
+
message: e.message,
|
|
50
|
+
cause: e
|
|
51
|
+
});
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
__name(loadNative, "loadNative");
|
|
56
|
+
var native;
|
|
57
|
+
function getNative(onError) {
|
|
58
|
+
if (native === void 0) native = loadNative(onError);
|
|
59
|
+
return native ?? null;
|
|
60
|
+
}
|
|
61
|
+
__name(getNative, "getNative");
|
|
62
|
+
async function enableNativeCapture(opts = {}) {
|
|
63
|
+
const mod = getNative(opts.onError);
|
|
64
|
+
if (!mod) return false;
|
|
65
|
+
try {
|
|
66
|
+
const ok = await mod.enableCrashReporter();
|
|
67
|
+
if (opts.enableAnrWatchdog && mod.enableAnrWatchdog) {
|
|
68
|
+
await mod.enableAnrWatchdog(opts.anrThresholdMs ?? 5e3);
|
|
69
|
+
}
|
|
70
|
+
return ok;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
opts.onError?.({
|
|
73
|
+
stage: "enable",
|
|
74
|
+
message: e.message,
|
|
75
|
+
cause: e
|
|
76
|
+
});
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
__name(enableNativeCapture, "enableNativeCapture");
|
|
81
|
+
async function getPendingNativeCrash(onError) {
|
|
82
|
+
const mod = getNative(onError);
|
|
83
|
+
if (!mod) return null;
|
|
84
|
+
try {
|
|
85
|
+
return await mod.getPendingCrashReport();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
onError?.({
|
|
88
|
+
stage: "get-report",
|
|
89
|
+
message: e.message,
|
|
90
|
+
cause: e
|
|
91
|
+
});
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
__name(getPendingNativeCrash, "getPendingNativeCrash");
|
|
96
|
+
async function purgePendingNativeCrash(onError) {
|
|
97
|
+
const mod = getNative(onError);
|
|
98
|
+
if (!mod) return;
|
|
99
|
+
try {
|
|
100
|
+
await mod.purgePendingCrashReport();
|
|
101
|
+
} catch (e) {
|
|
102
|
+
onError?.({
|
|
103
|
+
stage: "purge",
|
|
104
|
+
message: e.message,
|
|
105
|
+
cause: e
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
__name(purgePendingNativeCrash, "purgePendingNativeCrash");
|
|
110
|
+
async function generateTestNativeReport(onError) {
|
|
111
|
+
const mod = getNative(onError);
|
|
112
|
+
if (!mod?.generateTestReport) return null;
|
|
113
|
+
try {
|
|
114
|
+
return await mod.generateTestReport();
|
|
115
|
+
} catch (e) {
|
|
116
|
+
onError?.({
|
|
117
|
+
stage: "test-report",
|
|
118
|
+
message: e.message,
|
|
119
|
+
cause: e
|
|
120
|
+
});
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
__name(generateTestNativeReport, "generateTestNativeReport");
|
|
125
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
126
|
+
0 && (module.exports = {
|
|
127
|
+
enableNativeCapture,
|
|
128
|
+
generateTestNativeReport,
|
|
129
|
+
getPendingNativeCrash,
|
|
130
|
+
purgePendingNativeCrash
|
|
131
|
+
});
|
|
132
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @koderlabs/tasks-sdk-rn-native — native crash + ANR capture for RN.\n *\n * Companion package to `@koderlabs/tasks-sdk-rn`. When installed and\n * registered via `app.json#plugins`, this package:\n *\n * iOS:\n * - Bundles PLCrashReporter as a Pod\n * - Catches Mach exceptions, BSD signals (SIGSEGV/SIGBUS/SIGABRT/...)\n * and ObjC NSExceptions\n * - Reports are decoded on next launch and shipped via the JS SDK\n *\n * Android:\n * - JNI signal handler (libtasks_native_crash.so) for SIGSEGV/SIGBUS/\n * SIGABRT/SIGFPE/SIGILL with backtrace addresses\n * - Optional ANR watchdog thread (off by default — needs explicit\n * opt-in because false-positives on slow devices are common)\n * - Reports written to filesDir, shipped on next launch\n *\n * The JS API is intentionally minimal — register early at app boot, then\n * the package operates passively until a crash happens.\n *\n * Limitations (acknowledged in README):\n * - Symbol resolution requires CLI source-map upload for .so files\n * (planned follow-up).\n * - Crashpad/Breakpad integration is NOT included — would replace the\n * custom signal handler for richer stack walks. Separate package.\n * - Re-entrant crashes inside the handler are not detected.\n */\n// We require this dynamically so Node tests don't try to load it.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ExpoModulesCore = { requireOptionalNativeModule: (name: string) => any };\n\ninterface NativeCrashReport {\n platform: 'ios' | 'android';\n kind?: 'native_signal' | 'anr' | 'nsexception';\n timestamp?: string;\n signal?: string;\n faultAddr?: string;\n exception?: { name?: string; reason?: string };\n threads?: Array<{ threadNumber: number; crashed: boolean; frames: Array<{ instructionPointer: number; symbol?: string | null }> }>;\n images?: Array<{ name: string; baseAddress: number; size: number; uuid: string }>;\n frames?: string[];\n stack?: string[];\n thresholdMs?: number;\n capturedAt?: string;\n}\n\ninterface TasksNativeModule {\n enableCrashReporter(): Promise<boolean>;\n enableAnrWatchdog?(thresholdMs: number): Promise<boolean>;\n getPendingCrashReport(): Promise<NativeCrashReport | null>;\n purgePendingCrashReport(): Promise<boolean>;\n generateTestReport?(): Promise<{ captured: boolean; timestamp: string }>;\n}\n\n/**\n * Native module handle. Returns `null` when:\n * - The package is installed but the native side hasn't been prebuilt\n * (managed Expo Go can't load native code — only EAS Build)\n * - The host platform is web / Node tests\n * In those cases the JS layer falls back to the SDK-RN-only behaviour.\n */\nfunction loadNative(onError?: NativeErrorHandler): TasksNativeModule | null {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const core = (typeof require !== 'undefined' ? require : null)?.('expo-modules-core') as ExpoModulesCore | null;\n if (!core?.requireOptionalNativeModule) {\n onError?.({ stage: 'require-core', message: 'expo-modules-core not available' });\n return null;\n }\n const mod = core.requireOptionalNativeModule('TasksSdkRnNative') as TasksNativeModule | null;\n if (!mod) onError?.({ stage: 'require-native', message: 'TasksSdkRnNative native module not present' });\n return mod;\n } catch (e) {\n onError?.({ stage: 'require-core', message: (e as Error).message, cause: e });\n return null;\n }\n}\n\n/**\n * Tagged error reason so callers can distinguish \"native not installed\" from\n * a runtime failure that genuinely silenced crash reporting.\n */\nexport interface NativeError {\n stage: 'require-core' | 'require-native' | 'enable' | 'get-report' | 'purge' | 'test-report';\n message: string;\n cause?: unknown;\n}\nexport type NativeErrorHandler = (err: NativeError) => void;\n\n// Lazy singleton — loaded on first enableNativeCapture() to keep module-import\n// cheap and to avoid web/Node bundlers walking expo-modules-core at build time.\nlet native: TasksNativeModule | null | undefined; // undefined = unloaded\n\nexport interface EnableOptions {\n /**\n * Enable Android ANR watchdog. Default `false` because soft watchdogs\n * are noisy on slow devices and the OS does its own ANR detection. Only\n * worth enabling during dev or for apps with strict perf budgets.\n */\n enableAnrWatchdog?: boolean;\n /** ANR threshold in ms. Default 5000 (matches OS-level Activity ANR). */\n anrThresholdMs?: number;\n /**\n * Surface load/runtime errors instead of silently returning false.\n * Operators rely on this to detect when crash capture stops working —\n * the \"no crash\" state is otherwise indistinguishable from \"decode failed\".\n */\n onError?: NativeErrorHandler;\n}\n\nfunction getNative(onError?: NativeErrorHandler): TasksNativeModule | null {\n if (native === undefined) native = loadNative(onError);\n return native ?? null;\n}\n\n/**\n * Install native crash + (optional) ANR handlers. Idempotent — second call\n * is a no-op at the native layer. Safe to call before user authentication\n * is established.\n */\nexport async function enableNativeCapture(opts: EnableOptions = {}): Promise<boolean> {\n const mod = getNative(opts.onError);\n if (!mod) return false;\n try {\n const ok = await mod.enableCrashReporter();\n if (opts.enableAnrWatchdog && mod.enableAnrWatchdog) {\n await mod.enableAnrWatchdog(opts.anrThresholdMs ?? 5000);\n }\n return ok;\n } catch (e) {\n opts.onError?.({ stage: 'enable', message: (e as Error).message, cause: e });\n return false;\n }\n}\n\n/**\n * Pull the previous-launch crash report. Returns `null` if no pending crash\n * was found, the native module isn't loaded, or the read failed. After the\n * caller has shipped the event upstream, call `purgePendingNativeCrash()`\n * to delete the on-disk report.\n */\nexport async function getPendingNativeCrash(onError?: NativeErrorHandler): Promise<NativeCrashReport | null> {\n const mod = getNative(onError);\n if (!mod) return null;\n try {\n return await mod.getPendingCrashReport();\n } catch (e) {\n onError?.({ stage: 'get-report', message: (e as Error).message, cause: e });\n return null;\n }\n}\n\n/**\n * Delete the on-disk crash report so it isn't shipped a second time.\n * Always safe to call — no-op when nothing's there.\n */\nexport async function purgePendingNativeCrash(onError?: NativeErrorHandler): Promise<void> {\n const mod = getNative(onError);\n if (!mod) return;\n try {\n await mod.purgePendingCrashReport();\n } catch (e) {\n onError?.({ stage: 'purge', message: (e as Error).message, cause: e });\n }\n}\n\n/**\n * Generate a test crash report without crashing the process. Used for\n * verifying integration end-to-end during dev / CI.\n */\nexport async function generateTestNativeReport(onError?: NativeErrorHandler): Promise<{ captured: boolean; timestamp: string } | null> {\n const mod = getNative(onError);\n if (!mod?.generateTestReport) return null;\n try {\n return await mod.generateTestReport();\n } catch (e) {\n onError?.({ stage: 'test-report', message: (e as Error).message, cause: e });\n return null;\n }\n}\n\nexport type { NativeCrashReport };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;AA+DA,SAASA,WAAWC,SAA4B;AAC9C,MAAI;AAEF,UAAMC,QAAQ,OAAOC,YAAY,cAAcA,UAAU,QAAQ,mBAAA;AACjE,QAAI,CAACD,MAAME,6BAA6B;AACtCH,gBAAU;QAAEI,OAAO;QAAgBC,SAAS;MAAkC,CAAA;AAC9E,aAAO;IACT;AACA,UAAMC,MAAML,KAAKE,4BAA4B,kBAAA;AAC7C,QAAI,CAACG,IAAKN,WAAU;MAAEI,OAAO;MAAkBC,SAAS;IAA6C,CAAA;AACrG,WAAOC;EACT,SAASC,GAAG;AACVP,cAAU;MAAEI,OAAO;MAAgBC,SAAUE,EAAYF;MAASG,OAAOD;IAAE,CAAA;AAC3E,WAAO;EACT;AACF;AAfSR;AA8BT,IAAIU;AAmBJ,SAASC,UAAUV,SAA4B;AAC7C,MAAIS,WAAWE,OAAWF,UAASV,WAAWC,OAAAA;AAC9C,SAAOS,UAAU;AACnB;AAHSC;AAUT,eAAsBE,oBAAoBC,OAAsB,CAAC,GAAC;AAChE,QAAMP,MAAMI,UAAUG,KAAKb,OAAO;AAClC,MAAI,CAACM,IAAK,QAAO;AACjB,MAAI;AACF,UAAMQ,KAAK,MAAMR,IAAIS,oBAAmB;AACxC,QAAIF,KAAKG,qBAAqBV,IAAIU,mBAAmB;AACnD,YAAMV,IAAIU,kBAAkBH,KAAKI,kBAAkB,GAAA;IACrD;AACA,WAAOH;EACT,SAASP,GAAG;AACVM,SAAKb,UAAU;MAAEI,OAAO;MAAUC,SAAUE,EAAYF;MAASG,OAAOD;IAAE,CAAA;AAC1E,WAAO;EACT;AACF;AAbsBK;AAqBtB,eAAsBM,sBAAsBlB,SAA4B;AACtE,QAAMM,MAAMI,UAAUV,OAAAA;AACtB,MAAI,CAACM,IAAK,QAAO;AACjB,MAAI;AACF,WAAO,MAAMA,IAAIa,sBAAqB;EACxC,SAASZ,GAAG;AACVP,cAAU;MAAEI,OAAO;MAAcC,SAAUE,EAAYF;MAASG,OAAOD;IAAE,CAAA;AACzE,WAAO;EACT;AACF;AATsBW;AAetB,eAAsBE,wBAAwBpB,SAA4B;AACxE,QAAMM,MAAMI,UAAUV,OAAAA;AACtB,MAAI,CAACM,IAAK;AACV,MAAI;AACF,UAAMA,IAAIe,wBAAuB;EACnC,SAASd,GAAG;AACVP,cAAU;MAAEI,OAAO;MAASC,SAAUE,EAAYF;MAASG,OAAOD;IAAE,CAAA;EACtE;AACF;AARsBa;AActB,eAAsBE,yBAAyBtB,SAA4B;AACzE,QAAMM,MAAMI,UAAUV,OAAAA;AACtB,MAAI,CAACM,KAAKiB,mBAAoB,QAAO;AACrC,MAAI;AACF,WAAO,MAAMjB,IAAIiB,mBAAkB;EACrC,SAAShB,GAAG;AACVP,cAAU;MAAEI,OAAO;MAAeC,SAAUE,EAAYF;MAASG,OAAOD;IAAE,CAAA;AAC1E,WAAO;EACT;AACF;AATsBe;","names":["loadNative","onError","core","require","requireOptionalNativeModule","stage","message","mod","e","cause","native","getNative","undefined","enableNativeCapture","opts","ok","enableCrashReporter","enableAnrWatchdog","anrThresholdMs","getPendingNativeCrash","getPendingCrashReport","purgePendingNativeCrash","purgePendingCrashReport","generateTestNativeReport","generateTestReport"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
interface NativeCrashReport {
|
|
2
|
+
platform: 'ios' | 'android';
|
|
3
|
+
kind?: 'native_signal' | 'anr' | 'nsexception';
|
|
4
|
+
timestamp?: string;
|
|
5
|
+
signal?: string;
|
|
6
|
+
faultAddr?: string;
|
|
7
|
+
exception?: {
|
|
8
|
+
name?: string;
|
|
9
|
+
reason?: string;
|
|
10
|
+
};
|
|
11
|
+
threads?: Array<{
|
|
12
|
+
threadNumber: number;
|
|
13
|
+
crashed: boolean;
|
|
14
|
+
frames: Array<{
|
|
15
|
+
instructionPointer: number;
|
|
16
|
+
symbol?: string | null;
|
|
17
|
+
}>;
|
|
18
|
+
}>;
|
|
19
|
+
images?: Array<{
|
|
20
|
+
name: string;
|
|
21
|
+
baseAddress: number;
|
|
22
|
+
size: number;
|
|
23
|
+
uuid: string;
|
|
24
|
+
}>;
|
|
25
|
+
frames?: string[];
|
|
26
|
+
stack?: string[];
|
|
27
|
+
thresholdMs?: number;
|
|
28
|
+
capturedAt?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Tagged error reason so callers can distinguish "native not installed" from
|
|
32
|
+
* a runtime failure that genuinely silenced crash reporting.
|
|
33
|
+
*/
|
|
34
|
+
interface NativeError {
|
|
35
|
+
stage: 'require-core' | 'require-native' | 'enable' | 'get-report' | 'purge' | 'test-report';
|
|
36
|
+
message: string;
|
|
37
|
+
cause?: unknown;
|
|
38
|
+
}
|
|
39
|
+
type NativeErrorHandler = (err: NativeError) => void;
|
|
40
|
+
interface EnableOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Enable Android ANR watchdog. Default `false` because soft watchdogs
|
|
43
|
+
* are noisy on slow devices and the OS does its own ANR detection. Only
|
|
44
|
+
* worth enabling during dev or for apps with strict perf budgets.
|
|
45
|
+
*/
|
|
46
|
+
enableAnrWatchdog?: boolean;
|
|
47
|
+
/** ANR threshold in ms. Default 5000 (matches OS-level Activity ANR). */
|
|
48
|
+
anrThresholdMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Surface load/runtime errors instead of silently returning false.
|
|
51
|
+
* Operators rely on this to detect when crash capture stops working —
|
|
52
|
+
* the "no crash" state is otherwise indistinguishable from "decode failed".
|
|
53
|
+
*/
|
|
54
|
+
onError?: NativeErrorHandler;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Install native crash + (optional) ANR handlers. Idempotent — second call
|
|
58
|
+
* is a no-op at the native layer. Safe to call before user authentication
|
|
59
|
+
* is established.
|
|
60
|
+
*/
|
|
61
|
+
declare function enableNativeCapture(opts?: EnableOptions): Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* Pull the previous-launch crash report. Returns `null` if no pending crash
|
|
64
|
+
* was found, the native module isn't loaded, or the read failed. After the
|
|
65
|
+
* caller has shipped the event upstream, call `purgePendingNativeCrash()`
|
|
66
|
+
* to delete the on-disk report.
|
|
67
|
+
*/
|
|
68
|
+
declare function getPendingNativeCrash(onError?: NativeErrorHandler): Promise<NativeCrashReport | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Delete the on-disk crash report so it isn't shipped a second time.
|
|
71
|
+
* Always safe to call — no-op when nothing's there.
|
|
72
|
+
*/
|
|
73
|
+
declare function purgePendingNativeCrash(onError?: NativeErrorHandler): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Generate a test crash report without crashing the process. Used for
|
|
76
|
+
* verifying integration end-to-end during dev / CI.
|
|
77
|
+
*/
|
|
78
|
+
declare function generateTestNativeReport(onError?: NativeErrorHandler): Promise<{
|
|
79
|
+
captured: boolean;
|
|
80
|
+
timestamp: string;
|
|
81
|
+
} | null>;
|
|
82
|
+
|
|
83
|
+
export { type EnableOptions, type NativeCrashReport, type NativeError, type NativeErrorHandler, enableNativeCapture, generateTestNativeReport, getPendingNativeCrash, purgePendingNativeCrash };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
interface NativeCrashReport {
|
|
2
|
+
platform: 'ios' | 'android';
|
|
3
|
+
kind?: 'native_signal' | 'anr' | 'nsexception';
|
|
4
|
+
timestamp?: string;
|
|
5
|
+
signal?: string;
|
|
6
|
+
faultAddr?: string;
|
|
7
|
+
exception?: {
|
|
8
|
+
name?: string;
|
|
9
|
+
reason?: string;
|
|
10
|
+
};
|
|
11
|
+
threads?: Array<{
|
|
12
|
+
threadNumber: number;
|
|
13
|
+
crashed: boolean;
|
|
14
|
+
frames: Array<{
|
|
15
|
+
instructionPointer: number;
|
|
16
|
+
symbol?: string | null;
|
|
17
|
+
}>;
|
|
18
|
+
}>;
|
|
19
|
+
images?: Array<{
|
|
20
|
+
name: string;
|
|
21
|
+
baseAddress: number;
|
|
22
|
+
size: number;
|
|
23
|
+
uuid: string;
|
|
24
|
+
}>;
|
|
25
|
+
frames?: string[];
|
|
26
|
+
stack?: string[];
|
|
27
|
+
thresholdMs?: number;
|
|
28
|
+
capturedAt?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Tagged error reason so callers can distinguish "native not installed" from
|
|
32
|
+
* a runtime failure that genuinely silenced crash reporting.
|
|
33
|
+
*/
|
|
34
|
+
interface NativeError {
|
|
35
|
+
stage: 'require-core' | 'require-native' | 'enable' | 'get-report' | 'purge' | 'test-report';
|
|
36
|
+
message: string;
|
|
37
|
+
cause?: unknown;
|
|
38
|
+
}
|
|
39
|
+
type NativeErrorHandler = (err: NativeError) => void;
|
|
40
|
+
interface EnableOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Enable Android ANR watchdog. Default `false` because soft watchdogs
|
|
43
|
+
* are noisy on slow devices and the OS does its own ANR detection. Only
|
|
44
|
+
* worth enabling during dev or for apps with strict perf budgets.
|
|
45
|
+
*/
|
|
46
|
+
enableAnrWatchdog?: boolean;
|
|
47
|
+
/** ANR threshold in ms. Default 5000 (matches OS-level Activity ANR). */
|
|
48
|
+
anrThresholdMs?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Surface load/runtime errors instead of silently returning false.
|
|
51
|
+
* Operators rely on this to detect when crash capture stops working —
|
|
52
|
+
* the "no crash" state is otherwise indistinguishable from "decode failed".
|
|
53
|
+
*/
|
|
54
|
+
onError?: NativeErrorHandler;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Install native crash + (optional) ANR handlers. Idempotent — second call
|
|
58
|
+
* is a no-op at the native layer. Safe to call before user authentication
|
|
59
|
+
* is established.
|
|
60
|
+
*/
|
|
61
|
+
declare function enableNativeCapture(opts?: EnableOptions): Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* Pull the previous-launch crash report. Returns `null` if no pending crash
|
|
64
|
+
* was found, the native module isn't loaded, or the read failed. After the
|
|
65
|
+
* caller has shipped the event upstream, call `purgePendingNativeCrash()`
|
|
66
|
+
* to delete the on-disk report.
|
|
67
|
+
*/
|
|
68
|
+
declare function getPendingNativeCrash(onError?: NativeErrorHandler): Promise<NativeCrashReport | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Delete the on-disk crash report so it isn't shipped a second time.
|
|
71
|
+
* Always safe to call — no-op when nothing's there.
|
|
72
|
+
*/
|
|
73
|
+
declare function purgePendingNativeCrash(onError?: NativeErrorHandler): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Generate a test crash report without crashing the process. Used for
|
|
76
|
+
* verifying integration end-to-end during dev / CI.
|
|
77
|
+
*/
|
|
78
|
+
declare function generateTestNativeReport(onError?: NativeErrorHandler): Promise<{
|
|
79
|
+
captured: boolean;
|
|
80
|
+
timestamp: string;
|
|
81
|
+
} | null>;
|
|
82
|
+
|
|
83
|
+
export { type EnableOptions, type NativeCrashReport, type NativeError, type NativeErrorHandler, enableNativeCapture, generateTestNativeReport, getPendingNativeCrash, purgePendingNativeCrash };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
5
|
+
}) : x)(function(x) {
|
|
6
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
7
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// src/index.ts
|
|
11
|
+
function loadNative(onError) {
|
|
12
|
+
try {
|
|
13
|
+
const core = (typeof __require !== "undefined" ? __require : null)?.("expo-modules-core");
|
|
14
|
+
if (!core?.requireOptionalNativeModule) {
|
|
15
|
+
onError?.({
|
|
16
|
+
stage: "require-core",
|
|
17
|
+
message: "expo-modules-core not available"
|
|
18
|
+
});
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const mod = core.requireOptionalNativeModule("TasksSdkRnNative");
|
|
22
|
+
if (!mod) onError?.({
|
|
23
|
+
stage: "require-native",
|
|
24
|
+
message: "TasksSdkRnNative native module not present"
|
|
25
|
+
});
|
|
26
|
+
return mod;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
onError?.({
|
|
29
|
+
stage: "require-core",
|
|
30
|
+
message: e.message,
|
|
31
|
+
cause: e
|
|
32
|
+
});
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
__name(loadNative, "loadNative");
|
|
37
|
+
var native;
|
|
38
|
+
function getNative(onError) {
|
|
39
|
+
if (native === void 0) native = loadNative(onError);
|
|
40
|
+
return native ?? null;
|
|
41
|
+
}
|
|
42
|
+
__name(getNative, "getNative");
|
|
43
|
+
async function enableNativeCapture(opts = {}) {
|
|
44
|
+
const mod = getNative(opts.onError);
|
|
45
|
+
if (!mod) return false;
|
|
46
|
+
try {
|
|
47
|
+
const ok = await mod.enableCrashReporter();
|
|
48
|
+
if (opts.enableAnrWatchdog && mod.enableAnrWatchdog) {
|
|
49
|
+
await mod.enableAnrWatchdog(opts.anrThresholdMs ?? 5e3);
|
|
50
|
+
}
|
|
51
|
+
return ok;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
opts.onError?.({
|
|
54
|
+
stage: "enable",
|
|
55
|
+
message: e.message,
|
|
56
|
+
cause: e
|
|
57
|
+
});
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
__name(enableNativeCapture, "enableNativeCapture");
|
|
62
|
+
async function getPendingNativeCrash(onError) {
|
|
63
|
+
const mod = getNative(onError);
|
|
64
|
+
if (!mod) return null;
|
|
65
|
+
try {
|
|
66
|
+
return await mod.getPendingCrashReport();
|
|
67
|
+
} catch (e) {
|
|
68
|
+
onError?.({
|
|
69
|
+
stage: "get-report",
|
|
70
|
+
message: e.message,
|
|
71
|
+
cause: e
|
|
72
|
+
});
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
__name(getPendingNativeCrash, "getPendingNativeCrash");
|
|
77
|
+
async function purgePendingNativeCrash(onError) {
|
|
78
|
+
const mod = getNative(onError);
|
|
79
|
+
if (!mod) return;
|
|
80
|
+
try {
|
|
81
|
+
await mod.purgePendingCrashReport();
|
|
82
|
+
} catch (e) {
|
|
83
|
+
onError?.({
|
|
84
|
+
stage: "purge",
|
|
85
|
+
message: e.message,
|
|
86
|
+
cause: e
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
__name(purgePendingNativeCrash, "purgePendingNativeCrash");
|
|
91
|
+
async function generateTestNativeReport(onError) {
|
|
92
|
+
const mod = getNative(onError);
|
|
93
|
+
if (!mod?.generateTestReport) return null;
|
|
94
|
+
try {
|
|
95
|
+
return await mod.generateTestReport();
|
|
96
|
+
} catch (e) {
|
|
97
|
+
onError?.({
|
|
98
|
+
stage: "test-report",
|
|
99
|
+
message: e.message,
|
|
100
|
+
cause: e
|
|
101
|
+
});
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
__name(generateTestNativeReport, "generateTestNativeReport");
|
|
106
|
+
export {
|
|
107
|
+
enableNativeCapture,
|
|
108
|
+
generateTestNativeReport,
|
|
109
|
+
getPendingNativeCrash,
|
|
110
|
+
purgePendingNativeCrash
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @koderlabs/tasks-sdk-rn-native — native crash + ANR capture for RN.\n *\n * Companion package to `@koderlabs/tasks-sdk-rn`. When installed and\n * registered via `app.json#plugins`, this package:\n *\n * iOS:\n * - Bundles PLCrashReporter as a Pod\n * - Catches Mach exceptions, BSD signals (SIGSEGV/SIGBUS/SIGABRT/...)\n * and ObjC NSExceptions\n * - Reports are decoded on next launch and shipped via the JS SDK\n *\n * Android:\n * - JNI signal handler (libtasks_native_crash.so) for SIGSEGV/SIGBUS/\n * SIGABRT/SIGFPE/SIGILL with backtrace addresses\n * - Optional ANR watchdog thread (off by default — needs explicit\n * opt-in because false-positives on slow devices are common)\n * - Reports written to filesDir, shipped on next launch\n *\n * The JS API is intentionally minimal — register early at app boot, then\n * the package operates passively until a crash happens.\n *\n * Limitations (acknowledged in README):\n * - Symbol resolution requires CLI source-map upload for .so files\n * (planned follow-up).\n * - Crashpad/Breakpad integration is NOT included — would replace the\n * custom signal handler for richer stack walks. Separate package.\n * - Re-entrant crashes inside the handler are not detected.\n */\n// We require this dynamically so Node tests don't try to load it.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype ExpoModulesCore = { requireOptionalNativeModule: (name: string) => any };\n\ninterface NativeCrashReport {\n platform: 'ios' | 'android';\n kind?: 'native_signal' | 'anr' | 'nsexception';\n timestamp?: string;\n signal?: string;\n faultAddr?: string;\n exception?: { name?: string; reason?: string };\n threads?: Array<{ threadNumber: number; crashed: boolean; frames: Array<{ instructionPointer: number; symbol?: string | null }> }>;\n images?: Array<{ name: string; baseAddress: number; size: number; uuid: string }>;\n frames?: string[];\n stack?: string[];\n thresholdMs?: number;\n capturedAt?: string;\n}\n\ninterface TasksNativeModule {\n enableCrashReporter(): Promise<boolean>;\n enableAnrWatchdog?(thresholdMs: number): Promise<boolean>;\n getPendingCrashReport(): Promise<NativeCrashReport | null>;\n purgePendingCrashReport(): Promise<boolean>;\n generateTestReport?(): Promise<{ captured: boolean; timestamp: string }>;\n}\n\n/**\n * Native module handle. Returns `null` when:\n * - The package is installed but the native side hasn't been prebuilt\n * (managed Expo Go can't load native code — only EAS Build)\n * - The host platform is web / Node tests\n * In those cases the JS layer falls back to the SDK-RN-only behaviour.\n */\nfunction loadNative(onError?: NativeErrorHandler): TasksNativeModule | null {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const core = (typeof require !== 'undefined' ? require : null)?.('expo-modules-core') as ExpoModulesCore | null;\n if (!core?.requireOptionalNativeModule) {\n onError?.({ stage: 'require-core', message: 'expo-modules-core not available' });\n return null;\n }\n const mod = core.requireOptionalNativeModule('TasksSdkRnNative') as TasksNativeModule | null;\n if (!mod) onError?.({ stage: 'require-native', message: 'TasksSdkRnNative native module not present' });\n return mod;\n } catch (e) {\n onError?.({ stage: 'require-core', message: (e as Error).message, cause: e });\n return null;\n }\n}\n\n/**\n * Tagged error reason so callers can distinguish \"native not installed\" from\n * a runtime failure that genuinely silenced crash reporting.\n */\nexport interface NativeError {\n stage: 'require-core' | 'require-native' | 'enable' | 'get-report' | 'purge' | 'test-report';\n message: string;\n cause?: unknown;\n}\nexport type NativeErrorHandler = (err: NativeError) => void;\n\n// Lazy singleton — loaded on first enableNativeCapture() to keep module-import\n// cheap and to avoid web/Node bundlers walking expo-modules-core at build time.\nlet native: TasksNativeModule | null | undefined; // undefined = unloaded\n\nexport interface EnableOptions {\n /**\n * Enable Android ANR watchdog. Default `false` because soft watchdogs\n * are noisy on slow devices and the OS does its own ANR detection. Only\n * worth enabling during dev or for apps with strict perf budgets.\n */\n enableAnrWatchdog?: boolean;\n /** ANR threshold in ms. Default 5000 (matches OS-level Activity ANR). */\n anrThresholdMs?: number;\n /**\n * Surface load/runtime errors instead of silently returning false.\n * Operators rely on this to detect when crash capture stops working —\n * the \"no crash\" state is otherwise indistinguishable from \"decode failed\".\n */\n onError?: NativeErrorHandler;\n}\n\nfunction getNative(onError?: NativeErrorHandler): TasksNativeModule | null {\n if (native === undefined) native = loadNative(onError);\n return native ?? null;\n}\n\n/**\n * Install native crash + (optional) ANR handlers. Idempotent — second call\n * is a no-op at the native layer. Safe to call before user authentication\n * is established.\n */\nexport async function enableNativeCapture(opts: EnableOptions = {}): Promise<boolean> {\n const mod = getNative(opts.onError);\n if (!mod) return false;\n try {\n const ok = await mod.enableCrashReporter();\n if (opts.enableAnrWatchdog && mod.enableAnrWatchdog) {\n await mod.enableAnrWatchdog(opts.anrThresholdMs ?? 5000);\n }\n return ok;\n } catch (e) {\n opts.onError?.({ stage: 'enable', message: (e as Error).message, cause: e });\n return false;\n }\n}\n\n/**\n * Pull the previous-launch crash report. Returns `null` if no pending crash\n * was found, the native module isn't loaded, or the read failed. After the\n * caller has shipped the event upstream, call `purgePendingNativeCrash()`\n * to delete the on-disk report.\n */\nexport async function getPendingNativeCrash(onError?: NativeErrorHandler): Promise<NativeCrashReport | null> {\n const mod = getNative(onError);\n if (!mod) return null;\n try {\n return await mod.getPendingCrashReport();\n } catch (e) {\n onError?.({ stage: 'get-report', message: (e as Error).message, cause: e });\n return null;\n }\n}\n\n/**\n * Delete the on-disk crash report so it isn't shipped a second time.\n * Always safe to call — no-op when nothing's there.\n */\nexport async function purgePendingNativeCrash(onError?: NativeErrorHandler): Promise<void> {\n const mod = getNative(onError);\n if (!mod) return;\n try {\n await mod.purgePendingCrashReport();\n } catch (e) {\n onError?.({ stage: 'purge', message: (e as Error).message, cause: e });\n }\n}\n\n/**\n * Generate a test crash report without crashing the process. Used for\n * verifying integration end-to-end during dev / CI.\n */\nexport async function generateTestNativeReport(onError?: NativeErrorHandler): Promise<{ captured: boolean; timestamp: string } | null> {\n const mod = getNative(onError);\n if (!mod?.generateTestReport) return null;\n try {\n return await mod.generateTestReport();\n } catch (e) {\n onError?.({ stage: 'test-report', message: (e as Error).message, cause: e });\n return null;\n }\n}\n\nexport type { NativeCrashReport };\n"],"mappings":";;;;;;;;;;AA+DA,SAASA,WAAWC,SAA4B;AAC9C,MAAI;AAEF,UAAMC,QAAQ,OAAOC,cAAY,cAAcA,YAAU,QAAQ,mBAAA;AACjE,QAAI,CAACD,MAAME,6BAA6B;AACtCH,gBAAU;QAAEI,OAAO;QAAgBC,SAAS;MAAkC,CAAA;AAC9E,aAAO;IACT;AACA,UAAMC,MAAML,KAAKE,4BAA4B,kBAAA;AAC7C,QAAI,CAACG,IAAKN,WAAU;MAAEI,OAAO;MAAkBC,SAAS;IAA6C,CAAA;AACrG,WAAOC;EACT,SAASC,GAAG;AACVP,cAAU;MAAEI,OAAO;MAAgBC,SAAUE,EAAYF;MAASG,OAAOD;IAAE,CAAA;AAC3E,WAAO;EACT;AACF;AAfSR;AA8BT,IAAIU;AAmBJ,SAASC,UAAUV,SAA4B;AAC7C,MAAIS,WAAWE,OAAWF,UAASV,WAAWC,OAAAA;AAC9C,SAAOS,UAAU;AACnB;AAHSC;AAUT,eAAsBE,oBAAoBC,OAAsB,CAAC,GAAC;AAChE,QAAMP,MAAMI,UAAUG,KAAKb,OAAO;AAClC,MAAI,CAACM,IAAK,QAAO;AACjB,MAAI;AACF,UAAMQ,KAAK,MAAMR,IAAIS,oBAAmB;AACxC,QAAIF,KAAKG,qBAAqBV,IAAIU,mBAAmB;AACnD,YAAMV,IAAIU,kBAAkBH,KAAKI,kBAAkB,GAAA;IACrD;AACA,WAAOH;EACT,SAASP,GAAG;AACVM,SAAKb,UAAU;MAAEI,OAAO;MAAUC,SAAUE,EAAYF;MAASG,OAAOD;IAAE,CAAA;AAC1E,WAAO;EACT;AACF;AAbsBK;AAqBtB,eAAsBM,sBAAsBlB,SAA4B;AACtE,QAAMM,MAAMI,UAAUV,OAAAA;AACtB,MAAI,CAACM,IAAK,QAAO;AACjB,MAAI;AACF,WAAO,MAAMA,IAAIa,sBAAqB;EACxC,SAASZ,GAAG;AACVP,cAAU;MAAEI,OAAO;MAAcC,SAAUE,EAAYF;MAASG,OAAOD;IAAE,CAAA;AACzE,WAAO;EACT;AACF;AATsBW;AAetB,eAAsBE,wBAAwBpB,SAA4B;AACxE,QAAMM,MAAMI,UAAUV,OAAAA;AACtB,MAAI,CAACM,IAAK;AACV,MAAI;AACF,UAAMA,IAAIe,wBAAuB;EACnC,SAASd,GAAG;AACVP,cAAU;MAAEI,OAAO;MAASC,SAAUE,EAAYF;MAASG,OAAOD;IAAE,CAAA;EACtE;AACF;AARsBa;AActB,eAAsBE,yBAAyBtB,SAA4B;AACzE,QAAMM,MAAMI,UAAUV,OAAAA;AACtB,MAAI,CAACM,KAAKiB,mBAAoB,QAAO;AACrC,MAAI;AACF,WAAO,MAAMjB,IAAIiB,mBAAkB;EACrC,SAAShB,GAAG;AACVP,cAAU;MAAEI,OAAO;MAAeC,SAAUE,EAAYF;MAASG,OAAOD;IAAE,CAAA;AAC1E,WAAO;EACT;AACF;AATsBe;","names":["loadNative","onError","core","require","requireOptionalNativeModule","stage","message","mod","e","cause","native","getNative","undefined","enableNativeCapture","opts","ok","enableCrashReporter","enableAnrWatchdog","anrThresholdMs","getPendingNativeCrash","getPendingCrashReport","purgePendingNativeCrash","purgePendingCrashReport","generateTestNativeReport","generateTestReport"]}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import CrashReporter
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Expo module that exposes PLCrashReporter to JavaScript.
|
|
6
|
+
*
|
|
7
|
+
* - `enableCrashReporter()` installs the Mach exception + signal handler
|
|
8
|
+
* on the first call. PLCrashReporter writes a binary `.plcrash` file to
|
|
9
|
+
* ~/Library/Caches/com.plausiblelabs.crashreporter.data/ on death.
|
|
10
|
+
* - `getPendingCrashReport()` returns the previous-run crash report as a
|
|
11
|
+
* JSON object (symbolicated to addresses + load-image layout). After the
|
|
12
|
+
* JS layer ships it, the caller invokes `purgePendingCrashReport()`.
|
|
13
|
+
*
|
|
14
|
+
* The native side never tries to POST events itself — that's the JS SDK's
|
|
15
|
+
* job. We just expose a thin window into the crash artifact.
|
|
16
|
+
*/
|
|
17
|
+
public class TasksSdkRnNativeModule: Module {
|
|
18
|
+
private var crashReporter: PLCrashReporter?
|
|
19
|
+
|
|
20
|
+
public func definition() -> ModuleDefinition {
|
|
21
|
+
Name("TasksSdkRnNative")
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Install PLCrashReporter. Safe to call multiple times — the second call
|
|
25
|
+
* is a no-op. Returns `false` if installation failed (e.g. another crash
|
|
26
|
+
* reporter is already attached).
|
|
27
|
+
*/
|
|
28
|
+
AsyncFunction("enableCrashReporter") { (promise: Promise) in
|
|
29
|
+
if self.crashReporter != nil {
|
|
30
|
+
promise.resolve(true)
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
// Use Mach exception server — catches everything except SIGKILL/SIGSTOP.
|
|
34
|
+
// BSD signals are caught as a fallback for the subset Mach can't see.
|
|
35
|
+
let config = PLCrashReporterConfig(
|
|
36
|
+
signalHandlerType: .mach,
|
|
37
|
+
symbolicationStrategy: .all
|
|
38
|
+
)
|
|
39
|
+
guard let reporter = PLCrashReporter(configuration: config) else {
|
|
40
|
+
promise.resolve(false)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
do {
|
|
44
|
+
try reporter.enableAndReturnError()
|
|
45
|
+
self.crashReporter = reporter
|
|
46
|
+
promise.resolve(true)
|
|
47
|
+
} catch {
|
|
48
|
+
promise.reject("E_CRASH_REPORTER", "Failed to enable: \(error.localizedDescription)")
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Returns the pending crash report from the previous launch as a
|
|
54
|
+
* dictionary, or `null` if none exists. The dictionary mirrors the
|
|
55
|
+
* cross-platform schema consumed by `@koderlabs/tasks-sdk-rn`.
|
|
56
|
+
*/
|
|
57
|
+
AsyncFunction("getPendingCrashReport") { (promise: Promise) in
|
|
58
|
+
guard let reporter = self.crashReporter ?? PLCrashReporter.shared() else {
|
|
59
|
+
promise.resolve(nil)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
guard reporter.hasPendingCrashReport() else {
|
|
63
|
+
promise.resolve(nil)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
do {
|
|
67
|
+
let data = try reporter.loadPendingCrashReportDataAndReturnError()
|
|
68
|
+
let report = try PLCrashReport(data: data)
|
|
69
|
+
|
|
70
|
+
var threads: [[String: Any]] = []
|
|
71
|
+
if let reportThreads = report.threads as? [PLCrashReportThreadInfo] {
|
|
72
|
+
for thread in reportThreads {
|
|
73
|
+
var frames: [[String: Any]] = []
|
|
74
|
+
if let stackFrames = thread.stackFrames as? [PLCrashReportStackFrameInfo] {
|
|
75
|
+
for frame in stackFrames {
|
|
76
|
+
frames.append([
|
|
77
|
+
"instructionPointer": NSNumber(value: frame.instructionPointer),
|
|
78
|
+
"symbol": frame.symbolInfo?.symbolName ?? NSNull(),
|
|
79
|
+
])
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
threads.append([
|
|
83
|
+
"threadNumber": NSNumber(value: thread.threadNumber),
|
|
84
|
+
"crashed": NSNumber(value: thread.crashed),
|
|
85
|
+
"frames": frames,
|
|
86
|
+
])
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
var images: [[String: Any]] = []
|
|
91
|
+
if let reportImages = report.images as? [PLCrashReportBinaryImageInfo] {
|
|
92
|
+
for img in reportImages {
|
|
93
|
+
images.append([
|
|
94
|
+
"name": img.imageName ?? "",
|
|
95
|
+
"baseAddress": NSNumber(value: img.imageBaseAddress),
|
|
96
|
+
"size": NSNumber(value: img.imageSize),
|
|
97
|
+
"uuid": img.imageUUID ?? "",
|
|
98
|
+
])
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let exception: [String: Any] = report.hasExceptionInfo ? [
|
|
103
|
+
"name": report.exceptionInfo?.exceptionName ?? "",
|
|
104
|
+
"reason": report.exceptionInfo?.exceptionReason ?? "",
|
|
105
|
+
] : [:]
|
|
106
|
+
|
|
107
|
+
let signal: [String: Any] = report.signalInfo != nil ? [
|
|
108
|
+
"name": report.signalInfo?.name ?? "",
|
|
109
|
+
"code": report.signalInfo?.code ?? "",
|
|
110
|
+
"address": NSNumber(value: report.signalInfo?.address ?? 0),
|
|
111
|
+
] : [:]
|
|
112
|
+
|
|
113
|
+
let payload: [String: Any] = [
|
|
114
|
+
"platform": "ios",
|
|
115
|
+
"timestamp": (report.systemInfo?.timestamp ?? Date()).iso8601String(),
|
|
116
|
+
"appBundleId": report.applicationInfo?.applicationIdentifier ?? "",
|
|
117
|
+
"appVersion": report.applicationInfo?.applicationVersion ?? "",
|
|
118
|
+
"osVersion": report.systemInfo?.operatingSystemVersion ?? "",
|
|
119
|
+
"exception": exception,
|
|
120
|
+
"signal": signal,
|
|
121
|
+
"threads": threads,
|
|
122
|
+
"images": images,
|
|
123
|
+
]
|
|
124
|
+
promise.resolve(payload)
|
|
125
|
+
} catch {
|
|
126
|
+
promise.reject("E_CRASH_LOAD", "Failed to load pending crash: \(error.localizedDescription)")
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Delete the pending crash report so it isn't re-shipped on next launch. */
|
|
131
|
+
AsyncFunction("purgePendingCrashReport") { (promise: Promise) in
|
|
132
|
+
let reporter = self.crashReporter ?? PLCrashReporter.shared()
|
|
133
|
+
reporter?.purgePendingCrashReport()
|
|
134
|
+
promise.resolve(true)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Synchronously force a test crash. Wraps PLCrashReporter's built-in
|
|
139
|
+
* `generateLiveReport` so dev workflows can verify wiring without
|
|
140
|
+
* actually dying.
|
|
141
|
+
*/
|
|
142
|
+
AsyncFunction("generateTestReport") { (promise: Promise) in
|
|
143
|
+
guard let reporter = self.crashReporter else {
|
|
144
|
+
promise.reject("E_NOT_ENABLED", "Call enableCrashReporter first")
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
do {
|
|
148
|
+
let data = try reporter.generateLiveReportAndReturnError()
|
|
149
|
+
let report = try PLCrashReport(data: data)
|
|
150
|
+
promise.resolve([
|
|
151
|
+
"captured": true,
|
|
152
|
+
"timestamp": (report.systemInfo?.timestamp ?? Date()).iso8601String(),
|
|
153
|
+
])
|
|
154
|
+
} catch {
|
|
155
|
+
promise.reject("E_LIVE_REPORT", "Failed: \(error.localizedDescription)")
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private extension Date {
|
|
162
|
+
func iso8601String() -> String {
|
|
163
|
+
let f = ISO8601DateFormatter()
|
|
164
|
+
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
165
|
+
return f.string(from: self)
|
|
166
|
+
}
|
|
167
|
+
}
|