@logbrew/react-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/README.md +133 -0
- package/examples/index.mjs +36 -0
- package/examples/package.json +10 -0
- package/examples/readme-example.mjs +60 -0
- package/examples/real-user-smoke.mjs +145 -0
- package/index.cjs +473 -0
- package/index.d.cts +215 -0
- package/index.d.ts +215 -0
- package/index.js +456 -0
- package/index.native.js +50 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# @logbrew/react-native
|
|
2
|
+
|
|
3
|
+
React Native helpers for the public LogBrew JavaScript SDK.
|
|
4
|
+
|
|
5
|
+
This package is intentionally thin. It keeps all event validation, retry, flush, and shutdown behavior in `@logbrew/sdk`, while adding mobile-friendly helpers for screen views, app-state changes, handled JavaScript errors, provider/hook usage, and explicit W3C trace propagation for mobile fetch calls.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @logbrew/sdk @logbrew/react-native react react-native
|
|
11
|
+
pnpm add @logbrew/sdk @logbrew/react-native react react-native
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Basic Usage
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import { AppState, Platform } from "react-native";
|
|
18
|
+
import {
|
|
19
|
+
captureScreenView,
|
|
20
|
+
createAppStateListener,
|
|
21
|
+
createLogBrewReactNativeClient
|
|
22
|
+
} from "@logbrew/react-native";
|
|
23
|
+
|
|
24
|
+
const client = createLogBrewReactNativeClient({
|
|
25
|
+
clientKey: "LOGBREW_CLIENT_KEY",
|
|
26
|
+
sdkName: "my-mobile-app",
|
|
27
|
+
sdkVersion: "0.1.0"
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
captureScreenView(client, "Checkout", {
|
|
31
|
+
platform: Platform,
|
|
32
|
+
appState: AppState,
|
|
33
|
+
timestamp: "2026-06-02T10:00:03Z"
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const stopListening = createAppStateListener(client, AppState, {
|
|
37
|
+
platform: Platform
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For mobile apps, prefer an app-scoped public key through `clientKey`. `apiKey` is still accepted for compatibility with lower-level SDK examples and tests.
|
|
42
|
+
|
|
43
|
+
## Error Capture
|
|
44
|
+
|
|
45
|
+
Use `captureReactNativeError()` in app-owned error boundaries, route handlers, async catch blocks, or global handlers. It records handled JavaScript errors as LogBrew issue events with React Native context and omits stack text by default:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { captureReactNativeError } from "@logbrew/react-native";
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await checkout();
|
|
52
|
+
} catch (error) {
|
|
53
|
+
captureReactNativeError(client, error, {
|
|
54
|
+
platform: Platform,
|
|
55
|
+
appState: AppState,
|
|
56
|
+
screen: "Checkout",
|
|
57
|
+
metadata: { flow: "checkout" }
|
|
58
|
+
});
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Set `includeStack: true` only when your app has decided stack text is safe to send. Non-`Error` thrown values are accepted and converted into issue messages so app error handlers do not need custom guards.
|
|
64
|
+
|
|
65
|
+
## Provider And Hooks
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { AppState, Platform } from "react-native";
|
|
69
|
+
import {
|
|
70
|
+
LogBrewNativeProvider,
|
|
71
|
+
useLogBrewNativeActions
|
|
72
|
+
} from "@logbrew/react-native";
|
|
73
|
+
|
|
74
|
+
function CheckoutScreen() {
|
|
75
|
+
const { captureScreenView } = useLogBrewNativeActions();
|
|
76
|
+
captureScreenView("Checkout");
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function App({ client }) {
|
|
81
|
+
return (
|
|
82
|
+
<LogBrewNativeProvider client={client} platform={Platform} appState={AppState}>
|
|
83
|
+
<CheckoutScreen />
|
|
84
|
+
</LogBrewNativeProvider>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The package ships a `react-native` entry that imports `AppState` and `Platform` for Metro, while the default Node entry accepts those dependencies explicitly. That keeps packaged examples and CI smoke tests runnable without pretending a Node process is a native runtime.
|
|
90
|
+
|
|
91
|
+
## Trace Propagation
|
|
92
|
+
|
|
93
|
+
Use `createTraceparentFetch()` when a React Native app should connect mobile fetch work to backend traces. Propagation is target-scoped by default: no `traceparent` header is attached unless the request URL matches `tracePropagationTargets`.
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
import {
|
|
97
|
+
createReactNativeTraceparent,
|
|
98
|
+
createTraceparentFetch
|
|
99
|
+
} from "@logbrew/react-native";
|
|
100
|
+
|
|
101
|
+
const tracedFetch = createTraceparentFetch({
|
|
102
|
+
traceparentFactory: () => createReactNativeTraceparent(),
|
|
103
|
+
tracePropagationTargets: [
|
|
104
|
+
"https://api.example.com/",
|
|
105
|
+
/^\/mobile-api\//
|
|
106
|
+
]
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await tracedFetch("https://api.example.com/checkout", {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers: { accept: "application/json" }
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`tracePropagationTargets` accepts strings, regular expressions, or `(url) => boolean` functions. Match narrowly so mobile requests do not send tracing headers to unrelated origins. If the API is cross-origin or behind a gateway, allow the `traceparent` request header there too.
|
|
116
|
+
|
|
117
|
+
## Packaged Examples
|
|
118
|
+
|
|
119
|
+
After install, these commands are available from a consumer app:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
node node_modules/@logbrew/react-native/examples/index.mjs --help
|
|
123
|
+
node node_modules/@logbrew/react-native/examples/index.mjs --list
|
|
124
|
+
node node_modules/@logbrew/react-native/examples/index.mjs readme-example
|
|
125
|
+
node node_modules/@logbrew/react-native/examples/index.mjs real-user-smoke
|
|
126
|
+
node node_modules/@logbrew/react-native/examples/index.mjs
|
|
127
|
+
npm --prefix node_modules/@logbrew/react-native/examples run help
|
|
128
|
+
npm --prefix node_modules/@logbrew/react-native/examples run list
|
|
129
|
+
npm --prefix node_modules/@logbrew/react-native/examples run readme-example
|
|
130
|
+
npm --prefix node_modules/@logbrew/react-native/examples run real-user-smoke
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The default launcher path runs `real-user-smoke`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const commands = new Map([
|
|
4
|
+
["readme-example", new URL("./readme-example.mjs", import.meta.url)],
|
|
5
|
+
["real-user-smoke", new URL("./real-user-smoke.mjs", import.meta.url)]
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
const command = process.argv[2] ?? "real-user-smoke";
|
|
9
|
+
|
|
10
|
+
if (command === "--help" || command === "-h") {
|
|
11
|
+
printHelp();
|
|
12
|
+
} else if (command === "--list") {
|
|
13
|
+
printList();
|
|
14
|
+
} else if (commands.has(command)) {
|
|
15
|
+
await import(commands.get(command));
|
|
16
|
+
} else {
|
|
17
|
+
console.error(`Unknown LogBrew React Native example: ${command}`);
|
|
18
|
+
printList();
|
|
19
|
+
process.exitCode = 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function printHelp() {
|
|
23
|
+
console.log("LogBrew React Native examples");
|
|
24
|
+
console.log("node node_modules/@logbrew/react-native/examples/index.mjs --list");
|
|
25
|
+
console.log("node node_modules/@logbrew/react-native/examples/index.mjs readme-example");
|
|
26
|
+
console.log("node node_modules/@logbrew/react-native/examples/index.mjs real-user-smoke");
|
|
27
|
+
console.log("node node_modules/@logbrew/react-native/examples/index.mjs");
|
|
28
|
+
console.log("npm --prefix node_modules/@logbrew/react-native/examples run list");
|
|
29
|
+
console.log("npm --prefix node_modules/@logbrew/react-native/examples run readme-example");
|
|
30
|
+
console.log("npm --prefix node_modules/@logbrew/react-native/examples run real-user-smoke");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function printList() {
|
|
34
|
+
console.log("readme-example -> node node_modules/@logbrew/react-native/examples/index.mjs readme-example");
|
|
35
|
+
console.log("real-user-smoke -> node node_modules/@logbrew/react-native/examples/index.mjs real-user-smoke");
|
|
36
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { RecordingTransport } from "@logbrew/sdk";
|
|
2
|
+
import {
|
|
3
|
+
captureScreenView,
|
|
4
|
+
createLogBrewReactNativeClient
|
|
5
|
+
} from "@logbrew/react-native";
|
|
6
|
+
|
|
7
|
+
const fakePlatform = {
|
|
8
|
+
OS: "ios",
|
|
9
|
+
Version: "18.0",
|
|
10
|
+
isPad: false,
|
|
11
|
+
constants: { isTesting: true }
|
|
12
|
+
};
|
|
13
|
+
const fakeAppState = { currentState: "active" };
|
|
14
|
+
const client = createLogBrewReactNativeClient({
|
|
15
|
+
clientKey: "LOGBREW_CLIENT_KEY",
|
|
16
|
+
sdkName: "logbrew-react-native-readme-example",
|
|
17
|
+
sdkVersion: "0.1.0"
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
addFullBatch(client);
|
|
21
|
+
captureScreenView(client, "Checkout", {
|
|
22
|
+
id: "evt_action_001",
|
|
23
|
+
timestamp: "2026-06-02T10:00:05Z",
|
|
24
|
+
platform: fakePlatform,
|
|
25
|
+
appState: fakeAppState,
|
|
26
|
+
metadata: { flow: "checkout" }
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
console.log(client.previewJson());
|
|
30
|
+
const response = await client.shutdown(RecordingTransport.alwaysAccept());
|
|
31
|
+
console.error(JSON.stringify({ ok: true, status: response.statusCode, attempts: response.attempts, events: 6 }));
|
|
32
|
+
|
|
33
|
+
function addFullBatch(client) {
|
|
34
|
+
client.release("evt_release_001", "2026-06-02T10:00:00Z", {
|
|
35
|
+
version: "1.2.3",
|
|
36
|
+
commit: "abc123def456",
|
|
37
|
+
notes: "Public release marker"
|
|
38
|
+
});
|
|
39
|
+
client.environment("evt_environment_001", "2026-06-02T10:00:01Z", {
|
|
40
|
+
name: "production",
|
|
41
|
+
region: "global"
|
|
42
|
+
});
|
|
43
|
+
client.issue("evt_issue_001", "2026-06-02T10:00:02Z", {
|
|
44
|
+
title: "Checkout timeout",
|
|
45
|
+
level: "error",
|
|
46
|
+
message: "Request timed out after retry budget"
|
|
47
|
+
});
|
|
48
|
+
client.log("evt_log_001", "2026-06-02T10:00:03Z", {
|
|
49
|
+
message: "worker started",
|
|
50
|
+
level: "info",
|
|
51
|
+
logger: "job-runner"
|
|
52
|
+
});
|
|
53
|
+
client.span("evt_span_001", "2026-06-02T10:00:04Z", {
|
|
54
|
+
name: "GET /health",
|
|
55
|
+
traceId: "trace_001",
|
|
56
|
+
spanId: "span_001",
|
|
57
|
+
status: "ok",
|
|
58
|
+
durationMs: 12.5
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { RecordingTransport } from "@logbrew/sdk";
|
|
2
|
+
import {
|
|
3
|
+
captureReactNativeError,
|
|
4
|
+
captureScreenView,
|
|
5
|
+
createAppStateListener,
|
|
6
|
+
createLogBrewReactNativeClient,
|
|
7
|
+
createReactNativeTraceparent,
|
|
8
|
+
createTraceparentFetch,
|
|
9
|
+
shouldPropagateTraceparent
|
|
10
|
+
} from "@logbrew/react-native";
|
|
11
|
+
|
|
12
|
+
const fakePlatform = {
|
|
13
|
+
OS: "android",
|
|
14
|
+
Version: 35,
|
|
15
|
+
isPad: false,
|
|
16
|
+
constants: { isTesting: true }
|
|
17
|
+
};
|
|
18
|
+
let appStateListener = null;
|
|
19
|
+
const fakeAppState = {
|
|
20
|
+
currentState: "active",
|
|
21
|
+
addEventListener(_type, listener) {
|
|
22
|
+
appStateListener = listener;
|
|
23
|
+
return {
|
|
24
|
+
remove() {
|
|
25
|
+
appStateListener = null;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const client = createLogBrewReactNativeClient({
|
|
32
|
+
clientKey: "LOGBREW_CLIENT_KEY",
|
|
33
|
+
sdkName: "logbrew-react-native-real-user-smoke",
|
|
34
|
+
sdkVersion: "0.1.0",
|
|
35
|
+
maxRetries: 1
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
addCoreEvents(client);
|
|
39
|
+
captureScreenView(client, "Checkout", {
|
|
40
|
+
id: "evt_action_001",
|
|
41
|
+
timestamp: "2026-06-02T10:00:05Z",
|
|
42
|
+
platform: fakePlatform,
|
|
43
|
+
appState: fakeAppState,
|
|
44
|
+
metadata: { flow: "checkout" }
|
|
45
|
+
});
|
|
46
|
+
const stopListening = createAppStateListener(client, fakeAppState, {
|
|
47
|
+
id: "evt_action_app_state_background",
|
|
48
|
+
timestamp: "2026-06-02T10:00:06Z",
|
|
49
|
+
platform: fakePlatform
|
|
50
|
+
});
|
|
51
|
+
appStateListener("background");
|
|
52
|
+
stopListening();
|
|
53
|
+
const handledError = new Error("Checkout failed on device");
|
|
54
|
+
captureReactNativeError(client, handledError, {
|
|
55
|
+
id: "evt_issue_react_native_error",
|
|
56
|
+
timestamp: "2026-06-02T10:00:07Z",
|
|
57
|
+
platform: fakePlatform,
|
|
58
|
+
appState: fakeAppState,
|
|
59
|
+
screen: "Checkout",
|
|
60
|
+
metadata: { flow: "checkout", handled: true }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const propagatedRequests = [];
|
|
64
|
+
const tracedFetch = createTraceparentFetch({
|
|
65
|
+
fetchImpl: async (input, init = {}) => {
|
|
66
|
+
propagatedRequests.push({ input, init });
|
|
67
|
+
return { status: 204 };
|
|
68
|
+
},
|
|
69
|
+
traceparentFactory: () => createReactNativeTraceparent({
|
|
70
|
+
randomValues: deterministicBytes
|
|
71
|
+
}),
|
|
72
|
+
tracePropagationTargets: ["https://api.example.test/", /^\/mobile-api\//u]
|
|
73
|
+
});
|
|
74
|
+
if (!shouldPropagateTraceparent("https://api.example.test/checkout", ["https://api.example.test/"])) {
|
|
75
|
+
throw new Error("expected API request to match trace propagation target");
|
|
76
|
+
}
|
|
77
|
+
if (shouldPropagateTraceparent("https://cdn.example.test/app.js", ["https://api.example.test/"])) {
|
|
78
|
+
throw new Error("expected CDN request not to match trace propagation target");
|
|
79
|
+
}
|
|
80
|
+
await tracedFetch("https://api.example.test/checkout", {
|
|
81
|
+
headers: { accept: "application/json", traceparent: "00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01" }
|
|
82
|
+
});
|
|
83
|
+
await tracedFetch("https://cdn.example.test/app.js", {
|
|
84
|
+
headers: { accept: "text/javascript" }
|
|
85
|
+
});
|
|
86
|
+
await tracedFetch("/mobile-api/cart");
|
|
87
|
+
const propagatedTraceparent = propagatedRequests[0].init.headers.traceparent;
|
|
88
|
+
if (propagatedTraceparent !== "00-0102030405060708090a0b0c0d0e0f10-0102030405060708-01") {
|
|
89
|
+
throw new Error(`unexpected propagated traceparent: ${propagatedTraceparent}`);
|
|
90
|
+
}
|
|
91
|
+
if (propagatedRequests[0].init.headers.accept !== "application/json") {
|
|
92
|
+
throw new Error("expected traced fetch to preserve existing headers");
|
|
93
|
+
}
|
|
94
|
+
if (propagatedRequests[1].init.headers?.traceparent !== undefined) {
|
|
95
|
+
throw new Error("unmatched requests should not receive traceparent");
|
|
96
|
+
}
|
|
97
|
+
if (propagatedRequests[2].init.headers.traceparent !== propagatedTraceparent) {
|
|
98
|
+
throw new Error("relative matched requests should receive traceparent");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const preview = client.previewJson();
|
|
102
|
+
const transport = new RecordingTransport([{ statusCode: 503 }, { statusCode: 202 }]);
|
|
103
|
+
const response = await client.shutdown(transport);
|
|
104
|
+
console.log(preview);
|
|
105
|
+
console.error(JSON.stringify({
|
|
106
|
+
ok: true,
|
|
107
|
+
status: response.statusCode,
|
|
108
|
+
attempts: response.attempts,
|
|
109
|
+
events: 8,
|
|
110
|
+
listenerRemoved: appStateListener === null,
|
|
111
|
+
propagatedTraceparent
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
function addCoreEvents(client) {
|
|
115
|
+
client.release("evt_release_001", "2026-06-02T10:00:00Z", {
|
|
116
|
+
version: "1.2.3",
|
|
117
|
+
commit: "abc123def456",
|
|
118
|
+
notes: "Public release marker"
|
|
119
|
+
});
|
|
120
|
+
client.environment("evt_environment_001", "2026-06-02T10:00:01Z", {
|
|
121
|
+
name: "production",
|
|
122
|
+
region: "global"
|
|
123
|
+
});
|
|
124
|
+
client.issue("evt_issue_001", "2026-06-02T10:00:02Z", {
|
|
125
|
+
title: "Checkout timeout",
|
|
126
|
+
level: "error",
|
|
127
|
+
message: "Request timed out after retry budget"
|
|
128
|
+
});
|
|
129
|
+
client.log("evt_log_001", "2026-06-02T10:00:03Z", {
|
|
130
|
+
message: "worker started",
|
|
131
|
+
level: "info",
|
|
132
|
+
logger: "job-runner"
|
|
133
|
+
});
|
|
134
|
+
client.span("evt_span_001", "2026-06-02T10:00:04Z", {
|
|
135
|
+
name: "GET /health",
|
|
136
|
+
traceId: "trace_001",
|
|
137
|
+
spanId: "span_001",
|
|
138
|
+
status: "ok",
|
|
139
|
+
durationMs: 12.5
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function deterministicBytes(length) {
|
|
144
|
+
return Uint8Array.from({ length }, (_value, index) => index + 1);
|
|
145
|
+
}
|