@logbrew/react-native 0.1.0 → 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/README.md +407 -18
- package/apollo.cjs +301 -0
- package/apollo.d.cts +70 -0
- package/apollo.d.ts +70 -0
- package/apollo.js +294 -0
- package/examples/apollo-link-spans.mjs +149 -0
- package/examples/index.mjs +29 -1
- package/examples/instrumentation-kit.mjs +304 -0
- package/examples/lifecycle-spans.mjs +131 -0
- package/examples/native-bridge-scope.mjs +107 -0
- package/examples/navigation-resource-spans.mjs +156 -0
- package/examples/package.json +8 -1
- package/examples/real-user-smoke.mjs +106 -9
- package/examples/resource-fetch-spans.mjs +133 -0
- package/examples/trace-correlation.mjs +135 -0
- package/index.cjs +625 -111
- package/index.d.cts +236 -0
- package/index.d.ts +236 -0
- package/index.js +613 -95
- package/index.native.js +18 -0
- package/instrumentation.cjs +639 -0
- package/instrumentation.d.cts +84 -0
- package/instrumentation.d.ts +84 -0
- package/instrumentation.js +634 -0
- package/lifecycle.cjs +129 -0
- package/lifecycle.d.cts +50 -0
- package/lifecycle.d.ts +50 -0
- package/lifecycle.js +121 -0
- package/metadata.cjs +175 -0
- package/metadata.js +165 -0
- package/metro.cjs +310 -0
- package/metro.d.cts +37 -0
- package/metro.d.ts +37 -0
- package/metro.js +6 -0
- package/native-bridge.cjs +127 -0
- package/native-bridge.d.cts +60 -0
- package/native-bridge.d.ts +60 -0
- package/native-bridge.js +125 -0
- package/package.json +113 -4
- package/release-artifacts.cjs +344 -0
- package/release-artifacts.d.cts +55 -0
- package/release-artifacts.d.ts +53 -0
- package/release-artifacts.js +8 -0
- package/resource-fetch.cjs +469 -0
- package/resource-fetch.d.cts +60 -0
- package/resource-fetch.d.ts +60 -0
- package/resource-fetch.js +464 -0
package/examples/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
+
}));
|