@logbrew/react-native 0.1.0 → 0.1.2

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.
Files changed (51) hide show
  1. package/README.md +431 -18
  2. package/apollo.cjs +301 -0
  3. package/apollo.d.cts +70 -0
  4. package/apollo.d.ts +70 -0
  5. package/apollo.js +294 -0
  6. package/examples/apollo-link-spans.mjs +149 -0
  7. package/examples/index.mjs +29 -1
  8. package/examples/instrumentation-kit.mjs +304 -0
  9. package/examples/lifecycle-spans.mjs +131 -0
  10. package/examples/native-bridge-scope.mjs +107 -0
  11. package/examples/navigation-resource-spans.mjs +156 -0
  12. package/examples/package.json +8 -1
  13. package/examples/real-user-smoke.mjs +106 -9
  14. package/examples/resource-fetch-spans.mjs +133 -0
  15. package/examples/trace-correlation.mjs +135 -0
  16. package/global-errors.cjs +366 -0
  17. package/global-errors.d.cts +63 -0
  18. package/global-errors.d.ts +63 -0
  19. package/global-errors.js +7 -0
  20. package/index.cjs +625 -111
  21. package/index.d.cts +236 -0
  22. package/index.d.ts +236 -0
  23. package/index.js +613 -95
  24. package/index.native.js +18 -0
  25. package/instrumentation.cjs +639 -0
  26. package/instrumentation.d.cts +84 -0
  27. package/instrumentation.d.ts +84 -0
  28. package/instrumentation.js +634 -0
  29. package/lifecycle.cjs +129 -0
  30. package/lifecycle.d.cts +50 -0
  31. package/lifecycle.d.ts +50 -0
  32. package/lifecycle.js +121 -0
  33. package/metadata.cjs +175 -0
  34. package/metadata.js +165 -0
  35. package/metro.cjs +310 -0
  36. package/metro.d.cts +37 -0
  37. package/metro.d.ts +37 -0
  38. package/metro.js +6 -0
  39. package/native-bridge.cjs +127 -0
  40. package/native-bridge.d.cts +60 -0
  41. package/native-bridge.d.ts +60 -0
  42. package/native-bridge.js +125 -0
  43. package/package.json +128 -4
  44. package/release-artifacts.cjs +344 -0
  45. package/release-artifacts.d.cts +55 -0
  46. package/release-artifacts.d.ts +53 -0
  47. package/release-artifacts.js +8 -0
  48. package/resource-fetch.cjs +469 -0
  49. package/resource-fetch.d.cts +60 -0
  50. package/resource-fetch.d.ts +60 -0
  51. package/resource-fetch.js +464 -0
@@ -2,9 +2,16 @@
2
2
  "private": true,
3
3
  "type": "module",
4
4
  "scripts": {
5
+ "apollo-link-spans": "node ./index.mjs apollo-link-spans",
5
6
  "help": "node ./index.mjs --help",
7
+ "instrumentation-kit": "node ./index.mjs instrumentation-kit",
8
+ "lifecycle-spans": "node ./index.mjs lifecycle-spans",
6
9
  "list": "node ./index.mjs --list",
10
+ "native-bridge-scope": "node ./index.mjs native-bridge-scope",
11
+ "navigation-resource-spans": "node ./index.mjs navigation-resource-spans",
7
12
  "readme-example": "node ./index.mjs readme-example",
8
- "real-user-smoke": "node ./index.mjs real-user-smoke"
13
+ "real-user-smoke": "node ./index.mjs real-user-smoke",
14
+ "resource-fetch-spans": "node ./index.mjs resource-fetch-spans",
15
+ "trace-correlation": "node ./index.mjs trace-correlation"
9
16
  }
10
17
  }
@@ -1,12 +1,19 @@
1
1
  import { RecordingTransport } from "@logbrew/sdk";
2
2
  import {
3
+ captureReactNativeAction,
3
4
  captureReactNativeError,
5
+ captureReactNativeNetwork,
4
6
  captureScreenView,
5
7
  createAppStateListener,
6
8
  createLogBrewReactNativeClient,
9
+ createReactNativeSpanAttributes,
10
+ createReactNativeTraceContext,
11
+ createReactNativeTraceHeaders,
7
12
  createReactNativeTraceparent,
8
13
  createTraceparentFetch,
9
- shouldPropagateTraceparent
14
+ getActiveLogBrewTrace,
15
+ shouldPropagateTraceparent,
16
+ withLogBrewTrace
10
17
  } from "@logbrew/react-native";
11
18
 
12
19
  const fakePlatform = {
@@ -34,19 +41,27 @@ const client = createLogBrewReactNativeClient({
34
41
  sdkVersion: "0.1.0",
35
42
  maxRetries: 1
36
43
  });
44
+ const trace = createReactNativeTraceContext({
45
+ traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
46
+ spanId: "b7ad6b7169203331"
47
+ });
48
+ const activeTraceparent = createReactNativeTraceHeaders(trace).traceparent;
37
49
 
38
50
  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" }
51
+ withLogBrewTrace(trace, () => {
52
+ captureScreenView(client, "Checkout", {
53
+ id: "evt_action_001",
54
+ timestamp: "2026-06-02T10:00:05Z",
55
+ platform: fakePlatform,
56
+ appState: fakeAppState,
57
+ metadata: { flow: "checkout" }
58
+ });
45
59
  });
46
60
  const stopListening = createAppStateListener(client, fakeAppState, {
47
61
  id: "evt_action_app_state_background",
48
62
  timestamp: "2026-06-02T10:00:06Z",
49
- platform: fakePlatform
63
+ platform: fakePlatform,
64
+ trace
50
65
  });
51
66
  appStateListener("background");
52
67
  stopListening();
@@ -57,6 +72,7 @@ captureReactNativeError(client, handledError, {
57
72
  platform: fakePlatform,
58
73
  appState: fakeAppState,
59
74
  screen: "Checkout",
75
+ trace,
60
76
  metadata: { flow: "checkout", handled: true }
61
77
  });
62
78
 
@@ -84,6 +100,19 @@ await tracedFetch("https://cdn.example.test/app.js", {
84
100
  headers: { accept: "text/javascript" }
85
101
  });
86
102
  await tracedFetch("/mobile-api/cart");
103
+ const activeTraceRequests = [];
104
+ let activeTraceFetchPromise;
105
+ withLogBrewTrace(trace, () => {
106
+ const activeTraceFetch = createTraceparentFetch({
107
+ fetchImpl: async (input, init = {}) => {
108
+ activeTraceRequests.push({ input, init });
109
+ return { status: 204 };
110
+ },
111
+ tracePropagationTargets: ["https://api.example.test/"]
112
+ });
113
+ activeTraceFetchPromise = activeTraceFetch("https://api.example.test/mobile");
114
+ });
115
+ await activeTraceFetchPromise;
87
116
  const propagatedTraceparent = propagatedRequests[0].init.headers.traceparent;
88
117
  if (propagatedTraceparent !== "00-0102030405060708090a0b0c0d0e0f10-0102030405060708-01") {
89
118
  throw new Error(`unexpected propagated traceparent: ${propagatedTraceparent}`);
@@ -97,6 +126,71 @@ if (propagatedRequests[1].init.headers?.traceparent !== undefined) {
97
126
  if (propagatedRequests[2].init.headers.traceparent !== propagatedTraceparent) {
98
127
  throw new Error("relative matched requests should receive traceparent");
99
128
  }
129
+ if (activeTraceRequests[0].init.headers.traceparent !== activeTraceparent) {
130
+ throw new Error(`expected active trace fetch to reuse current trace: ${activeTraceRequests[0].init.headers.traceparent}`);
131
+ }
132
+ if (getActiveLogBrewTrace() !== undefined) {
133
+ throw new Error("active trace should be cleared after scoped fetch");
134
+ }
135
+
136
+ const timelineClient = createLogBrewReactNativeClient({
137
+ clientKey: "LOGBREW_CLIENT_KEY",
138
+ sdkName: "logbrew-react-native-timeline-smoke",
139
+ sdkVersion: "0.1.0",
140
+ maxRetries: 1
141
+ });
142
+ captureReactNativeAction(timelineClient, {
143
+ id: "evt_native_action_checkout_submit",
144
+ timestamp: "2026-06-02T10:00:08Z",
145
+ platform: fakePlatform,
146
+ appState: fakeAppState,
147
+ name: "checkout.submit",
148
+ screen: "Checkout",
149
+ sessionId: "session_mobile_001",
150
+ traceId: "trace_mobile_001",
151
+ metadata: {
152
+ funnel: "checkout",
153
+ step: "submit",
154
+ nested: { dropped: true }
155
+ }
156
+ });
157
+ captureReactNativeNetwork(timelineClient, {
158
+ id: "evt_native_network_checkout",
159
+ timestamp: "2026-06-02T10:00:09Z",
160
+ platform: fakePlatform,
161
+ appState: fakeAppState,
162
+ method: "post",
163
+ routeTemplate: "/api/checkout?email=dev@example.test#pay",
164
+ statusCode: 503,
165
+ durationMs: 241,
166
+ screen: "Checkout",
167
+ sessionId: "session_mobile_001",
168
+ trace
169
+ });
170
+ timelineClient.span("evt_native_span_checkout", "2026-06-02T10:00:10Z", createReactNativeSpanAttributes({
171
+ name: "mobile.checkout",
172
+ status: "ok",
173
+ durationMs: 245,
174
+ trace
175
+ }));
176
+ const timelineEvents = JSON.parse(timelineClient.previewJson()).events;
177
+ if (timelineEvents.length !== 3) {
178
+ throw new Error(`expected three timeline events, got ${timelineEvents.length}`);
179
+ }
180
+ const timelineAction = timelineEvents[0].attributes;
181
+ if (timelineAction.metadata.source !== "react-native.action" || timelineAction.metadata.nested !== undefined) {
182
+ throw new Error(`unexpected action timeline metadata: ${JSON.stringify(timelineAction.metadata)}`);
183
+ }
184
+ const timelineNetwork = timelineEvents[1].attributes;
185
+ if (timelineNetwork.name !== "POST /api/checkout" || timelineNetwork.status !== "failure") {
186
+ throw new Error(`unexpected network timeline event: ${JSON.stringify(timelineNetwork)}`);
187
+ }
188
+ if (timelineNetwork.metadata.routeTemplate !== "/api/checkout" || timelineNetwork.metadata.method !== "POST") {
189
+ throw new Error(`expected sanitized network metadata: ${JSON.stringify(timelineNetwork.metadata)}`);
190
+ }
191
+ if (timelineNetwork.metadata.traceId !== trace.traceId || timelineEvents[2].attributes.traceId !== trace.traceId) {
192
+ throw new Error("expected network and span timeline events to share the trace");
193
+ }
100
194
 
101
195
  const preview = client.previewJson();
102
196
  const transport = new RecordingTransport([{ statusCode: 503 }, { statusCode: 202 }]);
@@ -108,7 +202,10 @@ console.error(JSON.stringify({
108
202
  attempts: response.attempts,
109
203
  events: 8,
110
204
  listenerRemoved: appStateListener === null,
111
- propagatedTraceparent
205
+ timelineEvents: timelineEvents.length,
206
+ networkAction: timelineNetwork.name,
207
+ propagatedTraceparent,
208
+ activeTraceparent
112
209
  }));
113
210
 
114
211
  function addCoreEvents(client) {
@@ -0,0 +1,133 @@
1
+ import { RecordingTransport } from "@logbrew/sdk";
2
+ import {
3
+ createLogBrewReactNativeClient,
4
+ createReactNativeTraceContext
5
+ } from "@logbrew/react-native";
6
+ import {
7
+ createReactNativeGraphQLMetadataFactory,
8
+ createReactNativeResourceFetch
9
+ } from "@logbrew/react-native/resource-fetch";
10
+
11
+ const platform = {
12
+ OS: "ios",
13
+ Version: "18.5",
14
+ isPad: false,
15
+ constants: { isTesting: true }
16
+ };
17
+ const appState = { currentState: "active" };
18
+ const client = createLogBrewReactNativeClient({
19
+ clientKey: "LOGBREW_CLIENT_KEY",
20
+ sdkName: "logbrew-react-native-resource-fetch-spans",
21
+ sdkVersion: "0.1.0",
22
+ maxRetries: 1
23
+ });
24
+ const trace = createReactNativeTraceContext({
25
+ traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
26
+ spanId: "c2ad6b7169204442"
27
+ });
28
+ const requests = [];
29
+ const timestamps = [
30
+ "2026-06-02T10:21:00Z",
31
+ "2026-06-02T10:21:01Z"
32
+ ];
33
+ const phaseTimes = [1000, 1041, 1167, 2000, 2031];
34
+
35
+ const resourceFetch = createReactNativeResourceFetch(client, {
36
+ fetchImpl: async (input, init = {}) => {
37
+ requests.push({ input, init });
38
+ if (String(input).includes("/api/fail")) {
39
+ throw new TypeError("network request failed");
40
+ }
41
+ return {
42
+ status: 202,
43
+ headers: {
44
+ get(name) {
45
+ return String(name).toLowerCase() === "content-length" ? "1536" : null;
46
+ }
47
+ }
48
+ };
49
+ },
50
+ metadata: { flow: "checkout", nested: { dropped: true } },
51
+ metadataFactory: createReactNativeGraphQLMetadataFactory(),
52
+ now: () => timestamps.shift(),
53
+ nowMs: () => phaseTimes.shift(),
54
+ platform,
55
+ appState,
56
+ screen: "Checkout",
57
+ sessionId: "session_mobile_001",
58
+ trace,
59
+ tracePropagationTargets: ["https://api.example.test/"]
60
+ });
61
+
62
+ await resourceFetch("https://api.example.test/api/checkout?email=dev@example.test", {
63
+ method: "POST",
64
+ headers: { accept: "application/json" },
65
+ body: JSON.stringify({
66
+ query: "mutation CheckoutSubmit($email: String!) { checkout(email: $email) { id } }",
67
+ variables: { email: "dev@example.test" }
68
+ })
69
+ });
70
+ try {
71
+ await resourceFetch("https://cdn.example.test/api/fail?debug=hidden", {
72
+ method: "GET"
73
+ });
74
+ } catch (error) {
75
+ if (!(error instanceof TypeError)) {
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ const events = JSON.parse(client.previewJson()).events;
81
+ if (events.length !== 2) {
82
+ throw new Error(`expected two resource fetch spans, got ${events.length}`);
83
+ }
84
+ if (requests[0].init.headers.traceparent !== `00-${trace.traceId}-${trace.spanId}-01`) {
85
+ throw new Error(`expected API request traceparent, got ${requests[0].init.headers.traceparent}`);
86
+ }
87
+ if (requests[1].init.headers?.traceparent !== undefined) {
88
+ throw new Error("non-target resource fetch should not receive traceparent");
89
+ }
90
+ const success = events[0].attributes;
91
+ if (
92
+ success.name !== "POST /api/checkout" ||
93
+ success.status !== "ok" ||
94
+ success.durationMs !== 167 ||
95
+ success.metadata.routeTemplate !== "/api/checkout" ||
96
+ success.metadata.responseStartDurationMs !== 41 ||
97
+ success.metadata.graphqlOperationName !== "CheckoutSubmit" ||
98
+ success.metadata.graphqlOperationType !== "mutation" ||
99
+ success.metadata.responseSizeBytes !== 1536 ||
100
+ success.metadata.graphqlVariables !== undefined ||
101
+ success.metadata.requestBody !== undefined ||
102
+ success.metadata.traceId !== trace.traceId ||
103
+ success.metadata.nested !== undefined
104
+ ) {
105
+ throw new Error(`unexpected success resource span: ${JSON.stringify(success)}`);
106
+ }
107
+ if (JSON.stringify(events).includes("dev@example.test") || JSON.stringify(events).includes("checkout(email")) {
108
+ throw new Error("GraphQL resource span leaked request body content");
109
+ }
110
+ const failure = events[1].attributes;
111
+ if (
112
+ failure.name !== "GET /api/fail" ||
113
+ failure.status !== "error" ||
114
+ failure.durationMs !== 31 ||
115
+ failure.metadata.routeTemplate !== "/api/fail" ||
116
+ failure.metadata.fetchErrorName !== "TypeError" ||
117
+ failure.metadata.traceId !== trace.traceId
118
+ ) {
119
+ throw new Error(`unexpected failed resource span: ${JSON.stringify(failure)}`);
120
+ }
121
+
122
+ const preview = client.previewJson();
123
+ const response = await client.shutdown(RecordingTransport.alwaysAccept());
124
+ console.log(preview);
125
+ console.error(JSON.stringify({
126
+ ok: true,
127
+ events: events.length,
128
+ status: response.statusCode,
129
+ successSpan: success.name,
130
+ failureSpan: failure.name,
131
+ propagatedTraceparent: requests[0].init.headers.traceparent,
132
+ traceId: trace.traceId
133
+ }));
@@ -0,0 +1,135 @@
1
+ import { RecordingTransport } from "@logbrew/sdk";
2
+ import {
3
+ captureReactNativeAction,
4
+ captureReactNativeError,
5
+ captureReactNativeNetwork,
6
+ captureScreenView,
7
+ createLogBrewReactNativeClient,
8
+ createReactNativeSpanAttributes,
9
+ createReactNativeTraceContext,
10
+ createTraceparentFetch,
11
+ getActiveLogBrewTrace,
12
+ withLogBrewTrace
13
+ } from "@logbrew/react-native";
14
+
15
+ const platform = {
16
+ OS: "ios",
17
+ Version: "18.0",
18
+ isPad: false,
19
+ constants: { isTesting: true }
20
+ };
21
+ const appState = { currentState: "active" };
22
+ const client = createLogBrewReactNativeClient({
23
+ clientKey: "LOGBREW_CLIENT_KEY",
24
+ sdkName: "logbrew-react-native-trace-correlation",
25
+ sdkVersion: "0.1.0",
26
+ maxRetries: 1
27
+ });
28
+
29
+ const trace = createReactNativeTraceContext({
30
+ traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
31
+ spanId: "b7ad6b7169203331"
32
+ });
33
+ const requests = [];
34
+ let tracedFetchPromise;
35
+
36
+ withLogBrewTrace(trace, (activeTrace) => {
37
+ captureScreenView(client, "Checkout", {
38
+ id: "evt_trace_screen_checkout",
39
+ timestamp: "2026-06-02T10:10:00Z",
40
+ platform,
41
+ appState,
42
+ metadata: { traceId: "spoofed_trace_id", flow: "checkout" }
43
+ });
44
+ captureReactNativeAction(client, {
45
+ id: "evt_trace_action_submit",
46
+ timestamp: "2026-06-02T10:10:01Z",
47
+ platform,
48
+ appState,
49
+ name: "checkout.submit",
50
+ screen: "Checkout"
51
+ });
52
+ captureReactNativeNetwork(client, {
53
+ id: "evt_trace_network_checkout",
54
+ timestamp: "2026-06-02T10:10:02Z",
55
+ platform,
56
+ appState,
57
+ method: "post",
58
+ routeTemplate: "/api/checkout?email=dev@example.test",
59
+ statusCode: 202,
60
+ durationMs: 128,
61
+ screen: "Checkout"
62
+ });
63
+ captureReactNativeError(client, new Error("Checkout failed on device"), {
64
+ id: "evt_trace_error_checkout",
65
+ timestamp: "2026-06-02T10:10:03Z",
66
+ platform,
67
+ appState,
68
+ screen: "Checkout"
69
+ });
70
+ client.span("evt_trace_span_checkout", "2026-06-02T10:10:04Z", createReactNativeSpanAttributes({
71
+ name: "mobile.checkout",
72
+ status: "ok",
73
+ durationMs: 132.5,
74
+ trace: activeTrace,
75
+ metadata: { screen: "Checkout" }
76
+ }));
77
+
78
+ const tracedFetch = createTraceparentFetch({
79
+ fetchImpl: async (input, init = {}) => {
80
+ requests.push({ input, init });
81
+ return { status: 204 };
82
+ },
83
+ tracePropagationTargets: ["https://api.example.test/"]
84
+ });
85
+ tracedFetchPromise = tracedFetch("https://api.example.test/checkout", {
86
+ headers: { accept: "application/json" }
87
+ });
88
+ });
89
+ await tracedFetchPromise;
90
+
91
+ const events = JSON.parse(client.previewJson()).events;
92
+ const propagatedTraceparent = requests[0]?.init?.headers?.traceparent;
93
+ if (getActiveLogBrewTrace() !== undefined) {
94
+ throw new Error("active trace should be cleared after scope");
95
+ }
96
+ if (events.length !== 5) {
97
+ throw new Error(`expected five trace-correlated events, got ${events.length}`);
98
+ }
99
+ for (const event of events) {
100
+ const metadata = event.attributes.metadata ?? {};
101
+ if (event.type === "span") {
102
+ if (event.attributes.traceId !== trace.traceId || event.attributes.spanId !== trace.spanId) {
103
+ throw new Error(`span was not correlated: ${JSON.stringify(event.attributes)}`);
104
+ }
105
+ continue;
106
+ }
107
+ if (metadata.traceId !== trace.traceId || metadata.spanId !== trace.spanId || metadata.parentSpanId !== trace.parentSpanId) {
108
+ throw new Error(`event was not trace-correlated: ${JSON.stringify(event)}`);
109
+ }
110
+ }
111
+ if (events[0].attributes.metadata.traceId === "spoofed_trace_id") {
112
+ throw new Error("active trace metadata should overwrite spoofed trace keys");
113
+ }
114
+ if (events[2].attributes.metadata.routeTemplate !== "/api/checkout") {
115
+ throw new Error("network route metadata should strip query text");
116
+ }
117
+ if (events[3].attributes.metadata.errorStack !== undefined) {
118
+ throw new Error("React Native error stack should remain opt-in");
119
+ }
120
+ if (propagatedTraceparent !== "00-4bf92f3577b34da6a3ce929d0e0e4736-b7ad6b7169203331-01") {
121
+ throw new Error(`unexpected outgoing traceparent: ${propagatedTraceparent}`);
122
+ }
123
+
124
+ const preview = client.previewJson();
125
+ const response = await client.shutdown(RecordingTransport.alwaysAccept());
126
+ console.log(preview);
127
+ console.error(JSON.stringify({
128
+ ok: true,
129
+ events: events.length,
130
+ status: response.statusCode,
131
+ traceId: trace.traceId,
132
+ spanId: trace.spanId,
133
+ parentSpanId: trace.parentSpanId,
134
+ propagatedTraceparent
135
+ }));