@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 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,10 @@
1
+ {
2
+ "private": true,
3
+ "type": "module",
4
+ "scripts": {
5
+ "help": "node ./index.mjs --help",
6
+ "list": "node ./index.mjs --list",
7
+ "readme-example": "node ./index.mjs readme-example",
8
+ "real-user-smoke": "node ./index.mjs real-user-smoke"
9
+ }
10
+ }
@@ -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
+ }