@allstak/react-native 0.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/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +7 -0
- package/dist/index.mjs.map +1 -0
- package/native/README.md +73 -0
- package/native/android/src/main/java/io/allstak/rn/AllStakCrashHandler.java +93 -0
- package/native/android/src/main/java/io/allstak/rn/AllStakRNModule.java +45 -0
- package/native/ios/AllStakCrashHandler.h +21 -0
- package/native/ios/AllStakCrashHandler.m +76 -0
- package/native/ios/AllStakRNModule.m +29 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 AllStak
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @allstak-io/react-native
|
|
2
|
+
|
|
3
|
+
AllStak React Native SDK — `ErrorUtils` + Hermes rejection tracking + Platform device tags.
|
|
4
|
+
Includes native Android (Kotlin) and iOS (Swift) crash capture modules under `./native/`.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
> **Auth required:** GitHub Packages requires a token with `read:packages` scope.
|
|
9
|
+
|
|
10
|
+
### 1. Configure `.npmrc`
|
|
11
|
+
|
|
12
|
+
```ini
|
|
13
|
+
@allstak-io:registry=https://npm.pkg.github.com
|
|
14
|
+
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_PAT
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 2. Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @allstak-io/react-native@0.1.1 @allstak-io/core@0.1.1
|
|
21
|
+
# react-native >=0.70 is an optional peer dep
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { AllStak } from '@allstak-io/core';
|
|
28
|
+
import { installReactNative } from '@allstak-io/react-native';
|
|
29
|
+
|
|
30
|
+
// Initialize the base SDK
|
|
31
|
+
AllStak.init({
|
|
32
|
+
apiKey: process.env.ALLSTAK_API_KEY!,
|
|
33
|
+
environment: 'production',
|
|
34
|
+
release: 'v1.0.0',
|
|
35
|
+
// ingest: 'https://api.allstak.sa' ← default
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Install RN-specific instrumentation
|
|
39
|
+
installReactNative({
|
|
40
|
+
// Hooks ErrorUtils for uncaught JS errors
|
|
41
|
+
// Installs Hermes rejection tracking
|
|
42
|
+
// Tags every event with Platform.OS and Platform.Version
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## What's captured automatically after `installReactNative()`
|
|
47
|
+
|
|
48
|
+
| Capability | Notes |
|
|
49
|
+
|-----------|-------|
|
|
50
|
+
| Uncaught JS errors | Via `ErrorUtils.setGlobalHandler` |
|
|
51
|
+
| Unhandled promise rejections | Via Hermes rejection tracking |
|
|
52
|
+
| `Platform.OS` / `Platform.Version` tags | Attached to every event |
|
|
53
|
+
|
|
54
|
+
## Native crash capture (Android/iOS)
|
|
55
|
+
|
|
56
|
+
Native modules for Java/Kotlin (Android) and Obj-C/Swift (iOS) crash capture are in `./native/`. See [`native/README.md`](./native/README.md) for platform-specific setup.
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
| Export | Description |
|
|
61
|
+
|--------|-------------|
|
|
62
|
+
| `AllStak` | Re-exported from `@allstak-io/core` |
|
|
63
|
+
| `installReactNative(opts?)` | Hooks ErrorUtils + Hermes + device tags |
|
|
64
|
+
| `ReactNativeInstallOptions` | Options type for `installReactNative` |
|
|
65
|
+
|
|
66
|
+
## GitHub Packages
|
|
67
|
+
|
|
68
|
+
- **Package:** `@allstak-io/react-native`
|
|
69
|
+
- **Registry:** `https://npm.pkg.github.com`
|
|
70
|
+
- **Repo:** [github.com/allstak-io/allstak-react-native](https://github.com/allstak-io/allstak-react-native)
|
|
71
|
+
- **Releases:** [github.com/allstak-io/allstak-react-native/releases](https://github.com/allstak-io/allstak-react-native/releases)
|
|
72
|
+
|
|
73
|
+
## Versioning
|
|
74
|
+
|
|
75
|
+
Tags must match `package.json` version exactly (e.g. `v0.1.1`). The release workflow fails if there's a mismatch.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AllStak, ReactNativeInstallOptions, installReactNative } from 'allstak-js/react-native';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AllStak, ReactNativeInstallOptions, installReactNative } from 'allstak-js/react-native';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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 __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AllStak: () => import_react_native.AllStak,
|
|
24
|
+
installReactNative: () => import_react_native.installReactNative
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var import_react_native = require("allstak-js/react-native");
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @allstak/react-native — React Native public API.\n *\n * Re-exports the RN integration from allstak-js/react-native:\n * - installReactNative({...}) — hooks ErrorUtils + Hermes rejection tracking\n * + Platform.OS tags\n *\n * Native-layer crash capture (Java/Kotlin on Android, Obj-C/Swift on iOS)\n * lives under the `native/` directory inside this package. See README.\n */\nexport { installReactNative, type ReactNativeInstallOptions, AllStak } from 'allstak-js/react-native';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,0BAA4E;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @allstak/react-native — React Native public API.\n *\n * Re-exports the RN integration from allstak-js/react-native:\n * - installReactNative({...}) — hooks ErrorUtils + Hermes rejection tracking\n * + Platform.OS tags\n *\n * Native-layer crash capture (Java/Kotlin on Android, Obj-C/Swift on iOS)\n * lives under the `native/` directory inside this package. See README.\n */\nexport { installReactNative, type ReactNativeInstallOptions, AllStak } from 'allstak-js/react-native';\n"],"mappings":";AAUA,SAAS,oBAAoD,eAAe;","names":[]}
|
package/native/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @allstak/react-native — native crash capture
|
|
2
|
+
|
|
3
|
+
**Status: SCAFFOLDED, requires real device/emulator verification.**
|
|
4
|
+
|
|
5
|
+
This directory contains the Android (Java) and iOS (Obj-C) native modules
|
|
6
|
+
that intercept uncaught platform-level crashes — the ones that can't be
|
|
7
|
+
caught from the JS layer via `ErrorUtils`.
|
|
8
|
+
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
| File | Purpose |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `android/src/main/java/io/allstak/rn/AllStakCrashHandler.java` | Installs `Thread.setDefaultUncaughtExceptionHandler`, serialises the crash to `SharedPreferences`, survives process death. |
|
|
14
|
+
| `android/src/main/java/io/allstak/rn/AllStakRNModule.java` | `ReactContextBaseJavaModule` exposing `install(release)` + `drainPendingCrash()` to JS. |
|
|
15
|
+
| `ios/AllStakCrashHandler.{h,m}` | `NSSetUncaughtExceptionHandler` → `NSUserDefaults`. |
|
|
16
|
+
| `ios/AllStakRNModule.m` | `RCTBridgeModule` exposing the same API. |
|
|
17
|
+
|
|
18
|
+
## JS-side drain
|
|
19
|
+
|
|
20
|
+
The `@allstak/react-native` package exports `drainPendingNativeCrashes(release?)`
|
|
21
|
+
which calls `NativeModules.AllStakNative.drainPendingCrash()`, parses the JSON
|
|
22
|
+
payload (already DTO-compatible with `/ingest/v1/errors`), and re-submits it via
|
|
23
|
+
`AllStak.captureException` so the crash appears as a regular error group tagged
|
|
24
|
+
`native.crash=true` + `device.os=android|ios` + `fatal=true`.
|
|
25
|
+
|
|
26
|
+
Call it once early in your app init (inside a `try/catch`), *after* `AllStak.init()`:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { AllStak, installReactNative, drainPendingNativeCrashes } from '@allstak/react-native';
|
|
30
|
+
|
|
31
|
+
AllStak.init({ apiKey: '...', release: 'my-app@1.2.3' });
|
|
32
|
+
installReactNative();
|
|
33
|
+
drainPendingNativeCrashes('my-app@1.2.3');
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## How to finish the integration (per-app, one-time)
|
|
37
|
+
|
|
38
|
+
### Bare React Native — Android
|
|
39
|
+
|
|
40
|
+
1. Copy `native/android/src/main/java/io/allstak/rn/*.java` into your app's
|
|
41
|
+
`android/app/src/main/java/...` (or a local autolinked module).
|
|
42
|
+
2. Create a `ReactPackage` that registers `AllStakRNModule` — add to
|
|
43
|
+
`getPackages()` in `MainApplication.java`.
|
|
44
|
+
3. In `MainApplication.onCreate`, call
|
|
45
|
+
`AllStakCrashHandler.install(this, BuildConfig.VERSION_NAME)` BEFORE
|
|
46
|
+
`SoLoader.init(...)` so the handler is armed before any RN code runs.
|
|
47
|
+
|
|
48
|
+
### Bare React Native — iOS
|
|
49
|
+
|
|
50
|
+
1. Drag `native/ios/AllStakCrashHandler.{h,m}` + `AllStakRNModule.m` into the
|
|
51
|
+
Xcode project.
|
|
52
|
+
2. Add `#import "AllStakCrashHandler.h"` to `AppDelegate.m` and call
|
|
53
|
+
`[AllStakCrashHandler installWithRelease:@"<release>"]` at the top of
|
|
54
|
+
`application:didFinishLaunchingWithOptions:` (before RCTBridge is built).
|
|
55
|
+
3. CocoaPods autolinking will wire the `RCTBridgeModule` automatically.
|
|
56
|
+
|
|
57
|
+
### Expo
|
|
58
|
+
|
|
59
|
+
Bare workflow only for now — Expo Go cannot load custom native modules.
|
|
60
|
+
Config-plugin wrapper is a planned follow-up.
|
|
61
|
+
|
|
62
|
+
## Verification checklist (device/emulator required)
|
|
63
|
+
|
|
64
|
+
- [ ] Android Java crash (throw from MainActivity.onResume) → app dies →
|
|
65
|
+
relaunch → dashboard shows error group with
|
|
66
|
+
`exceptionClass=RuntimeException`, `device.os=android`, `fatal=true`,
|
|
67
|
+
`stackTrace` visible.
|
|
68
|
+
- [ ] iOS Obj-C exception (`@throw [NSException exceptionWithName:...]`) →
|
|
69
|
+
same flow, `device.os=ios`.
|
|
70
|
+
|
|
71
|
+
None of the above can be verified from a browser Chrome MCP session.
|
|
72
|
+
Flag this in the report as *scaffolded, verification blocked on physical
|
|
73
|
+
build env*.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
package io.allstak.rn;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.content.SharedPreferences;
|
|
5
|
+
import android.os.Build;
|
|
6
|
+
import android.util.Log;
|
|
7
|
+
|
|
8
|
+
import org.json.JSONArray;
|
|
9
|
+
import org.json.JSONObject;
|
|
10
|
+
|
|
11
|
+
import java.io.PrintWriter;
|
|
12
|
+
import java.io.StringWriter;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Installs a {@link Thread.UncaughtExceptionHandler} that serialises the
|
|
16
|
+
* crash to SharedPreferences so it survives process death. On next app
|
|
17
|
+
* launch, {@link #drainPendingCrash(Context)} returns the stashed payload
|
|
18
|
+
* for the JS layer to ship to AllStak.
|
|
19
|
+
*
|
|
20
|
+
* The crash payload is DTO-compatible with /ingest/v1/errors:
|
|
21
|
+
* { exceptionClass, message, stackTrace: List<String>,
|
|
22
|
+
* metadata: { platform, device.os, device.osVersion, device.model,
|
|
23
|
+
* fatal, source, release }, ... }
|
|
24
|
+
*
|
|
25
|
+
* This class is platform-level: it does NOT depend on React Native so the
|
|
26
|
+
* handler continues to work even if the RN bridge is already torn down.
|
|
27
|
+
*
|
|
28
|
+
* SCAFFOLDED — requires real device/emulator run to fully verify.
|
|
29
|
+
*/
|
|
30
|
+
public final class AllStakCrashHandler {
|
|
31
|
+
private static final String TAG = "AllStakCrashHandler";
|
|
32
|
+
private static final String PREFS_NAME = "allstak_crashes";
|
|
33
|
+
private static final String PREFS_KEY = "pending_crash";
|
|
34
|
+
|
|
35
|
+
private AllStakCrashHandler() {}
|
|
36
|
+
|
|
37
|
+
public static void install(final Context appContext, final String release) {
|
|
38
|
+
final Context ctx = appContext.getApplicationContext();
|
|
39
|
+
final Thread.UncaughtExceptionHandler previous = Thread.getDefaultUncaughtExceptionHandler();
|
|
40
|
+
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
|
|
41
|
+
@Override
|
|
42
|
+
public void uncaughtException(Thread thread, Throwable throwable) {
|
|
43
|
+
try {
|
|
44
|
+
JSONObject payload = buildPayload(throwable, release);
|
|
45
|
+
SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
46
|
+
prefs.edit().putString(PREFS_KEY, payload.toString()).commit();
|
|
47
|
+
} catch (Throwable t) {
|
|
48
|
+
Log.e(TAG, "failed to stash crash", t);
|
|
49
|
+
}
|
|
50
|
+
if (previous != null) {
|
|
51
|
+
previous.uncaughtException(thread, throwable);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Returns the stashed crash JSON (or null) and clears it. */
|
|
58
|
+
public static String drainPendingCrash(Context context) {
|
|
59
|
+
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
60
|
+
String json = prefs.getString(PREFS_KEY, null);
|
|
61
|
+
prefs.edit().remove(PREFS_KEY).commit();
|
|
62
|
+
return json;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private static JSONObject buildPayload(Throwable t, String release) throws Exception {
|
|
66
|
+
StringWriter sw = new StringWriter();
|
|
67
|
+
t.printStackTrace(new PrintWriter(sw));
|
|
68
|
+
String full = sw.toString();
|
|
69
|
+
JSONArray stack = new JSONArray();
|
|
70
|
+
for (String line : full.split("\n")) {
|
|
71
|
+
String trimmed = line.trim();
|
|
72
|
+
if (!trimmed.isEmpty()) stack.put(trimmed);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
JSONObject metadata = new JSONObject();
|
|
76
|
+
metadata.put("platform", "react-native");
|
|
77
|
+
metadata.put("device.os", "android");
|
|
78
|
+
metadata.put("device.osVersion", String.valueOf(Build.VERSION.SDK_INT));
|
|
79
|
+
metadata.put("device.model", Build.MODEL == null ? "" : Build.MODEL);
|
|
80
|
+
metadata.put("device.manufacturer", Build.MANUFACTURER == null ? "" : Build.MANUFACTURER);
|
|
81
|
+
metadata.put("fatal", "true");
|
|
82
|
+
metadata.put("source", "android-UncaughtExceptionHandler");
|
|
83
|
+
|
|
84
|
+
JSONObject payload = new JSONObject();
|
|
85
|
+
payload.put("exceptionClass", t.getClass().getSimpleName());
|
|
86
|
+
payload.put("message", t.getMessage() == null ? t.toString() : t.getMessage());
|
|
87
|
+
payload.put("stackTrace", stack);
|
|
88
|
+
payload.put("level", "fatal");
|
|
89
|
+
if (release != null) payload.put("release", release);
|
|
90
|
+
payload.put("metadata", metadata);
|
|
91
|
+
return payload;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package io.allstak.rn;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
6
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
7
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
8
|
+
import com.facebook.react.bridge.Promise;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* React Native module bridging {@link AllStakCrashHandler}.
|
|
12
|
+
*
|
|
13
|
+
* JS side:
|
|
14
|
+
* NativeModules.AllStakNative.drainPendingCrash().then(json => ...)
|
|
15
|
+
*
|
|
16
|
+
* SCAFFOLDED — compiles against React Native; requires a bare RN app with
|
|
17
|
+
* autolinking to verify end-to-end.
|
|
18
|
+
*/
|
|
19
|
+
public class AllStakRNModule extends ReactContextBaseJavaModule {
|
|
20
|
+
public AllStakRNModule(ReactApplicationContext ctx) { super(ctx); }
|
|
21
|
+
|
|
22
|
+
@NonNull
|
|
23
|
+
@Override
|
|
24
|
+
public String getName() { return "AllStakNative"; }
|
|
25
|
+
|
|
26
|
+
@ReactMethod
|
|
27
|
+
public void install(String release, Promise promise) {
|
|
28
|
+
try {
|
|
29
|
+
AllStakCrashHandler.install(getReactApplicationContext(), release);
|
|
30
|
+
promise.resolve(true);
|
|
31
|
+
} catch (Throwable t) {
|
|
32
|
+
promise.reject("install-failed", t);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@ReactMethod
|
|
37
|
+
public void drainPendingCrash(Promise promise) {
|
|
38
|
+
try {
|
|
39
|
+
String json = AllStakCrashHandler.drainPendingCrash(getReactApplicationContext());
|
|
40
|
+
promise.resolve(json);
|
|
41
|
+
} catch (Throwable t) {
|
|
42
|
+
promise.reject("drain-failed", t);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// AllStakCrashHandler — iOS native crash capture for React Native.
|
|
2
|
+
//
|
|
3
|
+
// SCAFFOLDED: compiles against UIKit; requires an Xcode project + real
|
|
4
|
+
// device/simulator to verify end-to-end.
|
|
5
|
+
|
|
6
|
+
#import <Foundation/Foundation.h>
|
|
7
|
+
|
|
8
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
9
|
+
|
|
10
|
+
@interface AllStakCrashHandler : NSObject
|
|
11
|
+
|
|
12
|
+
/// Install the NSUncaughtExceptionHandler. Idempotent.
|
|
13
|
+
+ (void)installWithRelease:(nullable NSString *)release;
|
|
14
|
+
|
|
15
|
+
/// Returns the JSON payload stashed by the previous crash (or nil), and
|
|
16
|
+
/// clears it from NSUserDefaults.
|
|
17
|
+
+ (nullable NSString *)drainPendingCrash;
|
|
18
|
+
|
|
19
|
+
@end
|
|
20
|
+
|
|
21
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// AllStakCrashHandler.m — iOS uncaught exception capture.
|
|
2
|
+
//
|
|
3
|
+
// SCAFFOLDED: requires Xcode compile + real iOS simulator/device for
|
|
4
|
+
// end-to-end verification. Obj-C / UIKit imports are standard; no
|
|
5
|
+
// third-party dependencies.
|
|
6
|
+
|
|
7
|
+
#import "AllStakCrashHandler.h"
|
|
8
|
+
#import <UIKit/UIKit.h>
|
|
9
|
+
|
|
10
|
+
static NSString * const kAllStakPendingCrashKey = @"io.allstak.rn.pending_crash";
|
|
11
|
+
static NSString *gAllStakRelease = nil;
|
|
12
|
+
static NSUncaughtExceptionHandler *gAllStakPreviousHandler = NULL;
|
|
13
|
+
|
|
14
|
+
static void AllStakHandleUncaughtException(NSException *exception) {
|
|
15
|
+
@try {
|
|
16
|
+
NSMutableArray<NSString *> *stack = [NSMutableArray array];
|
|
17
|
+
for (NSString *line in [exception callStackSymbols]) {
|
|
18
|
+
NSString *trimmed = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
19
|
+
if (trimmed.length > 0) [stack addObject:trimmed];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
UIDevice *dev = [UIDevice currentDevice];
|
|
23
|
+
NSDictionary *metadata = @{
|
|
24
|
+
@"platform": @"react-native",
|
|
25
|
+
@"device.os": @"ios",
|
|
26
|
+
@"device.osVersion": dev.systemVersion ?: @"",
|
|
27
|
+
@"device.model": dev.model ?: @"",
|
|
28
|
+
@"device.name": dev.name ?: @"",
|
|
29
|
+
@"fatal": @"true",
|
|
30
|
+
@"source": @"ios-NSUncaughtExceptionHandler"
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
NSMutableDictionary *payload = [@{
|
|
34
|
+
@"exceptionClass": exception.name ?: @"NSException",
|
|
35
|
+
@"message": exception.reason ?: @"(no reason)",
|
|
36
|
+
@"stackTrace": stack,
|
|
37
|
+
@"level": @"fatal",
|
|
38
|
+
@"metadata": metadata,
|
|
39
|
+
} mutableCopy];
|
|
40
|
+
if (gAllStakRelease) payload[@"release"] = gAllStakRelease;
|
|
41
|
+
|
|
42
|
+
NSError *err = nil;
|
|
43
|
+
NSData *json = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&err];
|
|
44
|
+
if (json && !err) {
|
|
45
|
+
NSString *str = [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding];
|
|
46
|
+
[[NSUserDefaults standardUserDefaults] setObject:str forKey:kAllStakPendingCrashKey];
|
|
47
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
48
|
+
}
|
|
49
|
+
} @catch (NSException *ignored) {
|
|
50
|
+
// never re-raise from within the crash handler
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (gAllStakPreviousHandler) {
|
|
54
|
+
gAllStakPreviousHandler(exception);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@implementation AllStakCrashHandler
|
|
59
|
+
|
|
60
|
+
+ (void)installWithRelease:(NSString *)release {
|
|
61
|
+
@synchronized(self) {
|
|
62
|
+
if (release) gAllStakRelease = [release copy];
|
|
63
|
+
gAllStakPreviousHandler = NSGetUncaughtExceptionHandler();
|
|
64
|
+
NSSetUncaughtExceptionHandler(&AllStakHandleUncaughtException);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
+ (NSString *)drainPendingCrash {
|
|
69
|
+
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
70
|
+
NSString *json = [defaults stringForKey:kAllStakPendingCrashKey];
|
|
71
|
+
[defaults removeObjectForKey:kAllStakPendingCrashKey];
|
|
72
|
+
[defaults synchronize];
|
|
73
|
+
return json;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// RCTBridgeModule bridging AllStakCrashHandler to JS.
|
|
2
|
+
//
|
|
3
|
+
// SCAFFOLDED: requires React Native iOS project with CocoaPods autolinking
|
|
4
|
+
// to verify end-to-end.
|
|
5
|
+
|
|
6
|
+
#import <React/RCTBridgeModule.h>
|
|
7
|
+
#import "AllStakCrashHandler.h"
|
|
8
|
+
|
|
9
|
+
@interface AllStakRNModule : NSObject <RCTBridgeModule>
|
|
10
|
+
@end
|
|
11
|
+
|
|
12
|
+
@implementation AllStakRNModule
|
|
13
|
+
|
|
14
|
+
RCT_EXPORT_MODULE(AllStakNative);
|
|
15
|
+
|
|
16
|
+
RCT_EXPORT_METHOD(install:(NSString *)release
|
|
17
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
18
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
19
|
+
[AllStakCrashHandler installWithRelease:release];
|
|
20
|
+
resolve(@YES);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
RCT_EXPORT_METHOD(drainPendingCrash:(RCTPromiseResolveBlock)resolve
|
|
24
|
+
rejecter:(RCTPromiseRejectBlock)reject) {
|
|
25
|
+
NSString *json = [AllStakCrashHandler drainPendingCrash];
|
|
26
|
+
resolve(json ?: [NSNull null]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@end
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@allstak/react-native",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "AllStak React Native SDK — ErrorUtils + Hermes rejection tracking + device tags. Depends on @allstak-io/core. Native Android/iOS crash capture modules in ./native.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"native",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/allstak-io/allstak-react-native.git"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://allstak.sa",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/allstak-io/allstak-react-native/issues"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"allstak",
|
|
32
|
+
"observability",
|
|
33
|
+
"react-native",
|
|
34
|
+
"mobile",
|
|
35
|
+
"error-tracking"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean --sourcemap --platform browser --external react-native",
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"react-native": ">=0.70"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"react-native": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@allstak/core": "^0.1.1",
|
|
55
|
+
"@allstak/js": "^0.1.1"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"tsup": "^8.0.0",
|
|
59
|
+
"typescript": "^5.3.0"
|
|
60
|
+
}
|
|
61
|
+
}
|