@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/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"]}
@@ -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 };
@@ -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,9 @@
1
+ {
2
+ "platforms": ["ios", "android"],
3
+ "ios": {
4
+ "modules": ["TasksSdkRnNativeModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.tasksnative.TasksSdkRnNativeModule"]
8
+ }
9
+ }
@@ -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
+ }