@logbrew/react-native 0.1.6 → 0.1.8
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 +116 -8
- package/examples/readme-example.mjs +8 -1
- package/index.cjs +131 -5
- package/index.d.cts +52 -5
- package/index.d.ts +52 -5
- package/index.js +131 -5
- package/metro.cjs +89 -5
- package/metro.d.cts +24 -4
- package/metro.d.ts +24 -4
- package/metro.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,27 +22,96 @@ import { AppState, Platform } from "react-native";
|
|
|
22
22
|
import {
|
|
23
23
|
captureScreenView,
|
|
24
24
|
createAppStateListener,
|
|
25
|
-
createLogBrewReactNativeClient
|
|
25
|
+
createLogBrewReactNativeClient,
|
|
26
|
+
createReactNativeFetchTransport
|
|
26
27
|
} from "@logbrew/react-native";
|
|
27
28
|
|
|
29
|
+
// Expo example. Bare React Native apps can use the same public key from
|
|
30
|
+
// their app-owned configuration layer.
|
|
31
|
+
const clientKey = process.env.EXPO_PUBLIC_LOGBREW_CLIENT_KEY;
|
|
32
|
+
if (!clientKey) {
|
|
33
|
+
throw new Error("Set EXPO_PUBLIC_LOGBREW_CLIENT_KEY to the public app-scoped key");
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
const client = createLogBrewReactNativeClient({
|
|
29
|
-
clientKey
|
|
37
|
+
clientKey,
|
|
30
38
|
sdkName: "my-mobile-app",
|
|
31
|
-
sdkVersion: "0.1.0"
|
|
39
|
+
sdkVersion: "0.1.0",
|
|
40
|
+
transport: createReactNativeFetchTransport()
|
|
32
41
|
});
|
|
33
42
|
|
|
34
43
|
captureScreenView(client, "Checkout", {
|
|
35
44
|
platform: Platform,
|
|
36
|
-
appState: AppState
|
|
37
|
-
timestamp: "2026-06-02T10:00:03Z"
|
|
45
|
+
appState: AppState
|
|
38
46
|
});
|
|
39
47
|
|
|
40
48
|
const stopListening = createAppStateListener(client, AppState, {
|
|
49
|
+
flushOnBackground: true,
|
|
41
50
|
platform: Platform
|
|
42
51
|
});
|
|
52
|
+
|
|
53
|
+
export async function verifyLogBrewSetup() {
|
|
54
|
+
const timestamp = new Date().toISOString();
|
|
55
|
+
client.log(`evt_react_native_setup_${Date.now()}`, timestamp, {
|
|
56
|
+
level: "info",
|
|
57
|
+
message: "React Native setup check",
|
|
58
|
+
metadata: {
|
|
59
|
+
environment: __DEV__ ? "development" : "production",
|
|
60
|
+
service: "my-mobile-app"
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const receipt = await client.flush();
|
|
65
|
+
return {
|
|
66
|
+
delivery: "hosted_accepted",
|
|
67
|
+
statusCode: receipt.statusCode,
|
|
68
|
+
attempts: receipt.attempts,
|
|
69
|
+
batches: receipt.batches
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
For mobile apps, prefer an app-scoped public key through `clientKey`. Expo
|
|
75
|
+
inlines `EXPO_PUBLIC_*` values into the app, so never use a server key there.
|
|
76
|
+
`apiKey` is still accepted for compatibility with lower-level SDK examples.
|
|
77
|
+
|
|
78
|
+
Supplying `transport` enables the core SDK's bounded automatic delivery. It
|
|
79
|
+
coalesces concurrent work, retries transient failures, retains rejected
|
|
80
|
+
batches, and pauses repeated automatic sends after authentication, rate-limit,
|
|
81
|
+
or non-retryable failures. Do not add a second app-owned flush interval.
|
|
82
|
+
`flushOnBackground: true` requests one final flush when AppState becomes
|
|
83
|
+
`inactive` or `background`; a failure never escapes the AppState callback.
|
|
84
|
+
|
|
85
|
+
## Confirm Hosted Delivery And Event Visibility
|
|
86
|
+
|
|
87
|
+
Call `verifyLogBrewSetup()` once from a development-only button or setup
|
|
88
|
+
screen. A returned `hosted_accepted` receipt means the configured HTTPS intake
|
|
89
|
+
accepted every batch in that flush. Then use an authenticated CLI session to
|
|
90
|
+
confirm that the backend stored the event for the intended project:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
logbrew read logs --project <project_id> \
|
|
94
|
+
--search "React Native setup check" --since 1h --json
|
|
43
95
|
```
|
|
44
96
|
|
|
45
|
-
|
|
97
|
+
These checks have different meanings:
|
|
98
|
+
|
|
99
|
+
- `client.previewJson()` validates and displays the local queued payload.
|
|
100
|
+
- `RecordingTransport.alwaysAccept()` is a local recording transport. Its
|
|
101
|
+
synthetic HTTP `202` makes no network request and never indicates hosted
|
|
102
|
+
delivery.
|
|
103
|
+
- `createReactNativeFetchTransport()` returns the actual intake status to
|
|
104
|
+
`client.flush()`.
|
|
105
|
+
- An authenticated CLI read confirms that the accepted event is visible in the
|
|
106
|
+
selected project. An intake `2xx` alone does not confirm event visibility.
|
|
107
|
+
|
|
108
|
+
Use `client.deliveryHealth()` for content-free queue and delivery state. In
|
|
109
|
+
particular, inspect `deliveryState`, `lastOutcome`, `lastStatusClass`,
|
|
110
|
+
`pausedReason`, `queueEvents`, and `acceptedEvents`. A `401` pauses automatic
|
|
111
|
+
delivery with `pausedReason: "authentication"`; rotate or correct the public
|
|
112
|
+
client key before creating a new client. A `429` preserves the queue and
|
|
113
|
+
reports `pausedReason: "rate_limit"` plus the bounded retry signal exposed by
|
|
114
|
+
the failed flush.
|
|
46
115
|
|
|
47
116
|
## Product Actions And API Milestones
|
|
48
117
|
|
|
@@ -160,7 +229,30 @@ The callbacks match the common `(id, rejection)` and `(id)` tracker shapes, but
|
|
|
160
229
|
|
|
161
230
|
`onUnhandled()` emits a fixed-content issue and deliberately does not inspect or send the rejection value, raw runtime rejection ID, error message, stack, Promise, or arbitrary metadata. Numeric IDs and bounded strings are retained only in local memory for duplicate suppression and `onHandled()` health. The set defaults to 128 entries and can be configured from 1 to 1024 with `maxTrackedRejections`; old entries are evicted. Missing or unsafe IDs still produce an untracked privacy-safe report. `onHandled()` updates local health only and cannot retract an issue that was already queued. Use `health()` for frozen counters and the last bounded outcome. Capture and diagnostic failures never escape these callbacks.
|
|
162
231
|
|
|
163
|
-
When you prepare
|
|
232
|
+
When you prepare Expo release artifacts, create the Expo Metro config through
|
|
233
|
+
LogBrew. The helper uses Expo's pre-serialization hook, so each production
|
|
234
|
+
bundle receives Expo's final Debug ID before Hermes compilation. Apply
|
|
235
|
+
the React Native Worklets bundle-mode transform after
|
|
236
|
+
`getLogBrewExpoConfig()`, as shown:
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
// metro.config.js
|
|
240
|
+
const { getLogBrewExpoConfig } = require("@logbrew/react-native/metro");
|
|
241
|
+
const { getBundleModeMetroConfig } = require("react-native-worklets/bundleMode");
|
|
242
|
+
|
|
243
|
+
const config = getLogBrewExpoConfig(__dirname);
|
|
244
|
+
|
|
245
|
+
module.exports = getBundleModeMetroConfig(config);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Pass normal Expo Metro options directly to the helper. If the app owns a
|
|
249
|
+
custom `getDefaultConfig` function, pass it as the `getDefaultConfig` option.
|
|
250
|
+
Existing `unstable_beforeAssetSerializationPlugins` are preserved and run
|
|
251
|
+
before LogBrew's plugin.
|
|
252
|
+
|
|
253
|
+
Bare React Native apps should instead wrap the completed app-owned Metro
|
|
254
|
+
config once. Production bundles and source maps receive one matching Debug ID,
|
|
255
|
+
while development and hot-reload serialization remain unchanged:
|
|
164
256
|
|
|
165
257
|
```js
|
|
166
258
|
// metro.config.js
|
|
@@ -172,6 +264,11 @@ module.exports = withLogBrewMetroConfig(
|
|
|
172
264
|
);
|
|
173
265
|
```
|
|
174
266
|
|
|
267
|
+
Do not apply `withLogBrewMetroConfig()` to an Expo config. Expo static exports
|
|
268
|
+
return asset sets and can produce Hermes bytecode; the bare React Native
|
|
269
|
+
serializer wrapper stops with a recovery message that points to
|
|
270
|
+
`getLogBrewExpoConfig()` rather than producing an untraceable build.
|
|
271
|
+
|
|
175
272
|
Then use the same release identity when capturing the error. The Metro-injected runtime registry connects each matching parsed JavaScript frame to its Debug ID without another app option:
|
|
176
273
|
|
|
177
274
|
```js
|
|
@@ -186,7 +283,18 @@ captureReactNativeError(client, error, {
|
|
|
186
283
|
});
|
|
187
284
|
```
|
|
188
285
|
|
|
189
|
-
The
|
|
286
|
+
The Expo helper and bare wrapper add no network behavior. The bare wrapper
|
|
287
|
+
composes an existing custom serializer and is idempotent. A string-returning
|
|
288
|
+
custom serializer may preserve Metro's default bundle code; a serializer that
|
|
289
|
+
changes code must return `{ code, map }` so LogBrew cannot attach a mismatched
|
|
290
|
+
source map. If an advanced build pipeline cannot use either integration,
|
|
291
|
+
`debugIdMap` remains an explicit override and takes precedence over runtime
|
|
292
|
+
discovery. LogBrew records up to 32 ordered path-only generated frames with
|
|
293
|
+
matching Debug IDs, release/environment/service/runtime, and active trace IDs
|
|
294
|
+
when available. It strips query strings, hashes, hosts, and local absolute
|
|
295
|
+
paths from React Native frame data; raw stack text is still opt-in with
|
|
296
|
+
`includeStack: true`. Hosted source-map lookup remains backend-owned and
|
|
297
|
+
requires the matching uploaded release artifact.
|
|
190
298
|
|
|
191
299
|
## Provider And Hooks
|
|
192
300
|
|
|
@@ -28,7 +28,14 @@ captureScreenView(client, "Checkout", {
|
|
|
28
28
|
|
|
29
29
|
console.log(client.previewJson());
|
|
30
30
|
const response = await client.shutdown(RecordingTransport.alwaysAccept());
|
|
31
|
-
console.error(JSON.stringify({
|
|
31
|
+
console.error(JSON.stringify({
|
|
32
|
+
ok: true,
|
|
33
|
+
mode: "local_recording",
|
|
34
|
+
hostedAccepted: false,
|
|
35
|
+
status: response.statusCode,
|
|
36
|
+
attempts: response.attempts,
|
|
37
|
+
events: 6
|
|
38
|
+
}));
|
|
32
39
|
|
|
33
40
|
function addFullBatch(client) {
|
|
34
41
|
client.release("evt_release_001", "2026-06-02T10:00:00Z", {
|
package/index.cjs
CHANGED
|
@@ -4,7 +4,8 @@ const {
|
|
|
4
4
|
createTraceparent,
|
|
5
5
|
LogBrewClient,
|
|
6
6
|
parseTraceparent,
|
|
7
|
-
SdkError
|
|
7
|
+
SdkError,
|
|
8
|
+
TransportError
|
|
8
9
|
} = require("@logbrew/sdk");
|
|
9
10
|
const {
|
|
10
11
|
runtimeReactNativeDebugIdMap,
|
|
@@ -14,22 +15,83 @@ const {
|
|
|
14
15
|
|
|
15
16
|
const DEFAULT_SDK_NAME = "logbrew-react-native";
|
|
16
17
|
const DEFAULT_SDK_VERSION = "0.1.0";
|
|
18
|
+
const DEFAULT_ENDPOINT = "https://api.logbrew.co/v1/events";
|
|
19
|
+
const BACKGROUND_APP_STATES = new Set(["background", "inactive"]);
|
|
17
20
|
const LogBrewNativeContext = React.createContext(null);
|
|
18
21
|
const activeTraceScopes = [];
|
|
19
22
|
let nextTraceScopeId = 0;
|
|
20
23
|
|
|
21
24
|
function createLogBrewReactNativeClient({
|
|
25
|
+
automaticDelivery,
|
|
22
26
|
apiKey,
|
|
23
27
|
clientKey,
|
|
28
|
+
deliveryIntervalMs,
|
|
29
|
+
deliveryQueueThreshold,
|
|
30
|
+
maxBatchBytes,
|
|
31
|
+
maxBatchEvents,
|
|
32
|
+
maxQueueBytes,
|
|
33
|
+
maxQueueSize,
|
|
34
|
+
onEventDropped,
|
|
24
35
|
sdkName = DEFAULT_SDK_NAME,
|
|
25
36
|
sdkVersion = DEFAULT_SDK_VERSION,
|
|
26
|
-
maxRetries = 2
|
|
37
|
+
maxRetries = 2,
|
|
38
|
+
transport
|
|
27
39
|
}) {
|
|
28
40
|
const authKey = clientKey ?? apiKey;
|
|
29
41
|
if (!authKey) {
|
|
30
42
|
throw new SdkError("configuration_error", "createLogBrewReactNativeClient requires clientKey or apiKey");
|
|
31
43
|
}
|
|
32
|
-
return LogBrewClient.create({
|
|
44
|
+
return LogBrewClient.create({
|
|
45
|
+
apiKey: authKey,
|
|
46
|
+
automaticDelivery,
|
|
47
|
+
deliveryIntervalMs,
|
|
48
|
+
deliveryQueueThreshold,
|
|
49
|
+
maxBatchBytes,
|
|
50
|
+
maxBatchEvents,
|
|
51
|
+
maxQueueBytes,
|
|
52
|
+
maxQueueSize,
|
|
53
|
+
maxRetries,
|
|
54
|
+
onEventDropped,
|
|
55
|
+
sdkName,
|
|
56
|
+
sdkVersion,
|
|
57
|
+
transport
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createReactNativeFetchTransport({
|
|
62
|
+
endpoint = DEFAULT_ENDPOINT,
|
|
63
|
+
fetchImpl = defaultFetch(),
|
|
64
|
+
headers = {}
|
|
65
|
+
} = {}) {
|
|
66
|
+
validateDeliveryEndpoint(endpoint);
|
|
67
|
+
if (typeof fetchImpl !== "function") {
|
|
68
|
+
throw new SdkError("configuration_error", "createReactNativeFetchTransport requires fetch");
|
|
69
|
+
}
|
|
70
|
+
if (!headers || Array.isArray(headers) || typeof headers !== "object") {
|
|
71
|
+
throw new SdkError("configuration_error", "createReactNativeFetchTransport headers must be an object");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
async send(apiKey, body) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetchImpl(endpoint, {
|
|
78
|
+
body,
|
|
79
|
+
headers: {
|
|
80
|
+
...headers,
|
|
81
|
+
authorization: `Bearer ${apiKey}`,
|
|
82
|
+
"content-type": "application/json"
|
|
83
|
+
},
|
|
84
|
+
method: "POST"
|
|
85
|
+
});
|
|
86
|
+
const retryAfterMs = retryAfterMsFromHeaders(response?.headers);
|
|
87
|
+
return retryAfterMs === undefined
|
|
88
|
+
? { statusCode: response?.status, attempts: 1 }
|
|
89
|
+
: { statusCode: response?.status, attempts: 1, retryAfterMs };
|
|
90
|
+
} catch {
|
|
91
|
+
throw TransportError.network("LogBrew React Native delivery failed");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
33
95
|
}
|
|
34
96
|
|
|
35
97
|
function createReactNativeTraceparent({ randomValues = defaultRandomValues, spanId, traceFlags = "01", traceId } = {}) {
|
|
@@ -574,12 +636,34 @@ function createAppStateListener(client, appState, options = {}) {
|
|
|
574
636
|
if (!appState || typeof appState.addEventListener !== "function") {
|
|
575
637
|
throw new SdkError("configuration_error", "createAppStateListener requires AppState.addEventListener");
|
|
576
638
|
}
|
|
639
|
+
const {
|
|
640
|
+
flushOnBackground = false,
|
|
641
|
+
onFlushError,
|
|
642
|
+
...captureOptions
|
|
643
|
+
} = options;
|
|
644
|
+
if (typeof flushOnBackground !== "boolean") {
|
|
645
|
+
throw new SdkError("configuration_error", "createAppStateListener flushOnBackground must be a boolean");
|
|
646
|
+
}
|
|
647
|
+
if (onFlushError !== undefined && typeof onFlushError !== "function") {
|
|
648
|
+
throw new SdkError("configuration_error", "createAppStateListener onFlushError must be a function");
|
|
649
|
+
}
|
|
577
650
|
|
|
578
651
|
const subscription = appState.addEventListener("change", (nextState) => {
|
|
579
652
|
captureAppStateChange(client, nextState, {
|
|
580
|
-
...
|
|
653
|
+
...captureOptions,
|
|
581
654
|
appState
|
|
582
655
|
});
|
|
656
|
+
if (flushOnBackground && BACKGROUND_APP_STATES.has(nextState)) {
|
|
657
|
+
void client.flush().catch((error) => {
|
|
658
|
+
if (typeof onFlushError === "function") {
|
|
659
|
+
try {
|
|
660
|
+
onFlushError(error);
|
|
661
|
+
} catch {
|
|
662
|
+
// Delivery diagnostics must not interrupt the app-state callback.
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
}
|
|
583
667
|
});
|
|
584
668
|
|
|
585
669
|
return subscriptionRemover(subscription);
|
|
@@ -616,6 +700,9 @@ function useLogBrewNativeActions() {
|
|
|
616
700
|
action: (id, timestamp, attributes) => client.action(id, timestamp, attributesWithTrace(attributes, trace)),
|
|
617
701
|
flush: client.flush.bind(client),
|
|
618
702
|
shutdown: client.shutdown.bind(client),
|
|
703
|
+
deliveryHealth: client.deliveryHealth.bind(client),
|
|
704
|
+
droppedEvents: client.droppedEvents.bind(client),
|
|
705
|
+
pendingBytes: client.pendingBytes.bind(client),
|
|
619
706
|
previewJson: client.previewJson.bind(client),
|
|
620
707
|
pendingEvents: client.pendingEvents.bind(client),
|
|
621
708
|
trace,
|
|
@@ -699,6 +786,44 @@ function errorMessage(error) {
|
|
|
699
786
|
return String(error ?? "unknown error");
|
|
700
787
|
}
|
|
701
788
|
|
|
789
|
+
function validateDeliveryEndpoint(endpoint) {
|
|
790
|
+
if (typeof endpoint !== "string" || endpoint.trim() === "") {
|
|
791
|
+
throw new SdkError("configuration_error", "createReactNativeFetchTransport requires a non-empty endpoint");
|
|
792
|
+
}
|
|
793
|
+
let parsed;
|
|
794
|
+
try {
|
|
795
|
+
parsed = new URL(endpoint);
|
|
796
|
+
} catch {
|
|
797
|
+
throw new SdkError("configuration_error", "createReactNativeFetchTransport endpoint must be an absolute URL");
|
|
798
|
+
}
|
|
799
|
+
if (parsed.protocol !== "https:") {
|
|
800
|
+
throw new SdkError("configuration_error", "createReactNativeFetchTransport endpoint must use HTTPS");
|
|
801
|
+
}
|
|
802
|
+
if (endpoint !== `${parsed.origin}${parsed.pathname}`) {
|
|
803
|
+
throw new SdkError("configuration_error", "createReactNativeFetchTransport endpoint must be a plain HTTPS path");
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function retryAfterMsFromHeaders(headers) {
|
|
808
|
+
if (!headers || typeof headers.get !== "function") {
|
|
809
|
+
return undefined;
|
|
810
|
+
}
|
|
811
|
+
return retryAfterMsFromHeader(headers.get("retry-after"));
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function retryAfterMsFromHeader(value, now = Date.now()) {
|
|
815
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
const trimmed = value.trim();
|
|
819
|
+
if (/^\d+$/u.test(trimmed)) {
|
|
820
|
+
const milliseconds = Number(trimmed) * 1000;
|
|
821
|
+
return Number.isSafeInteger(milliseconds) ? milliseconds : undefined;
|
|
822
|
+
}
|
|
823
|
+
const timestamp = Date.parse(trimmed);
|
|
824
|
+
return Number.isFinite(timestamp) ? Math.max(0, timestamp - now) : undefined;
|
|
825
|
+
}
|
|
826
|
+
|
|
702
827
|
function defaultErrorEventId({ message, screen }) {
|
|
703
828
|
return `evt_native_error_${slugify(`${screen ?? "app"}_${message}`)}`;
|
|
704
829
|
}
|
|
@@ -977,7 +1102,8 @@ const defaultExport = {
|
|
|
977
1102
|
LogBrewNativeProvider, captureAppStateChange, captureReactNativeAction, captureReactNativeError,
|
|
978
1103
|
captureReactNativeNetwork, captureReactNativeNavigationSpan, captureReactNativeResourceSpan, captureScreenView,
|
|
979
1104
|
bindLogBrewTrace, createAppStateListener, createLogBrewReactNativeClient, createReactNavigationSpanListener,
|
|
980
|
-
createReactNativeSpanAttributes, createReactNativeTraceContext,
|
|
1105
|
+
createReactNativeFetchTransport, createReactNativeSpanAttributes, createReactNativeTraceContext,
|
|
1106
|
+
createReactNativeTraceHeaders, createReactNativeActionEvent,
|
|
981
1107
|
createReactNativeErrorEvent, createReactNativeNetworkEvent, createReactNativeNavigationSpanEvent,
|
|
982
1108
|
createReactNativeResourceSpanEvent, createReactNativeTraceparent, createTraceparentFetch, getActiveLogBrewTrace,
|
|
983
1109
|
getReactNativeContext, getReactNativeTraceMetadata, shouldPropagateTraceparent, useLogBrewNative,
|
package/index.d.cts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type * as React from "react";
|
|
2
2
|
import type {
|
|
3
3
|
ActionAttributes,
|
|
4
|
+
DeliveryHealthSnapshot,
|
|
5
|
+
DroppedEvent,
|
|
4
6
|
EnvironmentAttributes,
|
|
5
7
|
IssueAttributes,
|
|
6
8
|
LogAttributes,
|
|
@@ -30,11 +32,42 @@ export type ReactNativeAppStateLike = {
|
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
export type CreateLogBrewReactNativeClientConfig = {
|
|
35
|
+
automaticDelivery?: boolean;
|
|
33
36
|
apiKey?: string;
|
|
34
37
|
clientKey?: string;
|
|
38
|
+
deliveryIntervalMs?: number;
|
|
39
|
+
deliveryQueueThreshold?: number;
|
|
40
|
+
maxBatchBytes?: number;
|
|
41
|
+
maxBatchEvents?: number;
|
|
42
|
+
maxQueueBytes?: number;
|
|
43
|
+
maxQueueSize?: number;
|
|
44
|
+
maxRetries?: number;
|
|
45
|
+
onEventDropped?: (drop: DroppedEvent) => void;
|
|
35
46
|
sdkName?: string;
|
|
36
47
|
sdkVersion?: string;
|
|
37
|
-
|
|
48
|
+
transport?: Transport;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type ReactNativeFetchResponseLike = {
|
|
52
|
+
status: number;
|
|
53
|
+
headers?: {
|
|
54
|
+
get(name: string): string | null | undefined;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type ReactNativeFetchLike = (
|
|
59
|
+
endpoint: string,
|
|
60
|
+
init: {
|
|
61
|
+
body: string;
|
|
62
|
+
headers: Record<string, string>;
|
|
63
|
+
method: "POST";
|
|
64
|
+
}
|
|
65
|
+
) => Promise<ReactNativeFetchResponseLike> | ReactNativeFetchResponseLike;
|
|
66
|
+
|
|
67
|
+
export type ReactNativeFetchTransportConfig = {
|
|
68
|
+
endpoint?: string;
|
|
69
|
+
fetchImpl?: ReactNativeFetchLike;
|
|
70
|
+
headers?: Record<string, string>;
|
|
38
71
|
};
|
|
39
72
|
|
|
40
73
|
export type TracePropagationTarget = string | RegExp | ((url: string) => boolean);
|
|
@@ -122,6 +155,13 @@ export type CaptureAppStateChangeOptions = ReactNativeContextOptions & {
|
|
|
122
155
|
timestamp?: string;
|
|
123
156
|
};
|
|
124
157
|
|
|
158
|
+
export type CreateAppStateListenerOptions = CaptureAppStateChangeOptions & {
|
|
159
|
+
/** Flush the configured client transport when AppState becomes inactive or background. */
|
|
160
|
+
flushOnBackground?: boolean;
|
|
161
|
+
/** Observe a background flush failure without throwing from the AppState callback. */
|
|
162
|
+
onFlushError?: (error: unknown) => void;
|
|
163
|
+
};
|
|
164
|
+
|
|
125
165
|
export type ReactNativeActionEvent = {
|
|
126
166
|
id: string;
|
|
127
167
|
timestamp: string;
|
|
@@ -278,8 +318,11 @@ export type LogBrewNativeActions = {
|
|
|
278
318
|
log(id: string, timestamp: string, attributes: LogAttributes): void;
|
|
279
319
|
span(id: string, timestamp: string, attributes: SpanAttributes): void;
|
|
280
320
|
action(id: string, timestamp: string, attributes: ActionAttributes): void;
|
|
281
|
-
flush(transport
|
|
282
|
-
shutdown(transport
|
|
321
|
+
flush(transport?: Transport): Promise<TransportResponse>;
|
|
322
|
+
shutdown(transport?: Transport): Promise<TransportResponse>;
|
|
323
|
+
deliveryHealth(): DeliveryHealthSnapshot;
|
|
324
|
+
droppedEvents(): number;
|
|
325
|
+
pendingBytes(): number;
|
|
283
326
|
previewJson(): string;
|
|
284
327
|
pendingEvents(): number;
|
|
285
328
|
trace?: ReactNativeTraceContext;
|
|
@@ -295,6 +338,9 @@ export type LogBrewNativeActions = {
|
|
|
295
338
|
export declare function createLogBrewReactNativeClient(
|
|
296
339
|
config: CreateLogBrewReactNativeClientConfig
|
|
297
340
|
): LogBrewClient;
|
|
341
|
+
export declare function createReactNativeFetchTransport(
|
|
342
|
+
config?: ReactNativeFetchTransportConfig
|
|
343
|
+
): Transport;
|
|
298
344
|
export declare function createReactNativeTraceparent(config?: ReactNativeTraceparentConfig): string;
|
|
299
345
|
export declare function createReactNativeTraceContext(
|
|
300
346
|
config?: ReactNativeTraceContextConfig
|
|
@@ -378,7 +424,7 @@ export declare function captureReactNativeError(
|
|
|
378
424
|
export declare function createAppStateListener(
|
|
379
425
|
client: LogBrewClient,
|
|
380
426
|
appState: ReactNativeAppStateLike,
|
|
381
|
-
options?:
|
|
427
|
+
options?: CreateAppStateListenerOptions
|
|
382
428
|
): () => void;
|
|
383
429
|
export declare function LogBrewNativeProvider(props: LogBrewNativeProviderProps): React.ReactElement;
|
|
384
430
|
export declare function useLogBrewNative(): LogBrewNativeContextValue;
|
|
@@ -413,7 +459,7 @@ export declare function captureDefaultReactNativeError(
|
|
|
413
459
|
): ReactNativeErrorEvent;
|
|
414
460
|
export declare function createDefaultAppStateListener(
|
|
415
461
|
client: LogBrewClient,
|
|
416
|
-
options?: Omit<
|
|
462
|
+
options?: Omit<CreateAppStateListenerOptions, "appState">
|
|
417
463
|
): () => void;
|
|
418
464
|
|
|
419
465
|
declare const defaultExport: {
|
|
@@ -429,6 +475,7 @@ declare const defaultExport: {
|
|
|
429
475
|
createAppStateListener: typeof createAppStateListener;
|
|
430
476
|
createLogBrewReactNativeClient: typeof createLogBrewReactNativeClient;
|
|
431
477
|
createReactNavigationSpanListener: typeof createReactNavigationSpanListener;
|
|
478
|
+
createReactNativeFetchTransport: typeof createReactNativeFetchTransport;
|
|
432
479
|
createReactNativeSpanAttributes: typeof createReactNativeSpanAttributes;
|
|
433
480
|
createReactNativeTraceContext: typeof createReactNativeTraceContext;
|
|
434
481
|
createReactNativeTraceHeaders: typeof createReactNativeTraceHeaders;
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type * as React from "react";
|
|
2
2
|
import type {
|
|
3
3
|
ActionAttributes,
|
|
4
|
+
DeliveryHealthSnapshot,
|
|
5
|
+
DroppedEvent,
|
|
4
6
|
EnvironmentAttributes,
|
|
5
7
|
IssueAttributes,
|
|
6
8
|
LogAttributes,
|
|
@@ -30,11 +32,42 @@ export type ReactNativeAppStateLike = {
|
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
export type CreateLogBrewReactNativeClientConfig = {
|
|
35
|
+
automaticDelivery?: boolean;
|
|
33
36
|
apiKey?: string;
|
|
34
37
|
clientKey?: string;
|
|
38
|
+
deliveryIntervalMs?: number;
|
|
39
|
+
deliveryQueueThreshold?: number;
|
|
40
|
+
maxBatchBytes?: number;
|
|
41
|
+
maxBatchEvents?: number;
|
|
42
|
+
maxQueueBytes?: number;
|
|
43
|
+
maxQueueSize?: number;
|
|
44
|
+
maxRetries?: number;
|
|
45
|
+
onEventDropped?: (drop: DroppedEvent) => void;
|
|
35
46
|
sdkName?: string;
|
|
36
47
|
sdkVersion?: string;
|
|
37
|
-
|
|
48
|
+
transport?: Transport;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type ReactNativeFetchResponseLike = {
|
|
52
|
+
status: number;
|
|
53
|
+
headers?: {
|
|
54
|
+
get(name: string): string | null | undefined;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type ReactNativeFetchLike = (
|
|
59
|
+
endpoint: string,
|
|
60
|
+
init: {
|
|
61
|
+
body: string;
|
|
62
|
+
headers: Record<string, string>;
|
|
63
|
+
method: "POST";
|
|
64
|
+
}
|
|
65
|
+
) => Promise<ReactNativeFetchResponseLike> | ReactNativeFetchResponseLike;
|
|
66
|
+
|
|
67
|
+
export type ReactNativeFetchTransportConfig = {
|
|
68
|
+
endpoint?: string;
|
|
69
|
+
fetchImpl?: ReactNativeFetchLike;
|
|
70
|
+
headers?: Record<string, string>;
|
|
38
71
|
};
|
|
39
72
|
|
|
40
73
|
export type TracePropagationTarget = string | RegExp | ((url: string) => boolean);
|
|
@@ -122,6 +155,13 @@ export type CaptureAppStateChangeOptions = ReactNativeContextOptions & {
|
|
|
122
155
|
timestamp?: string;
|
|
123
156
|
};
|
|
124
157
|
|
|
158
|
+
export type CreateAppStateListenerOptions = CaptureAppStateChangeOptions & {
|
|
159
|
+
/** Flush the configured client transport when AppState becomes inactive or background. */
|
|
160
|
+
flushOnBackground?: boolean;
|
|
161
|
+
/** Observe a background flush failure without throwing from the AppState callback. */
|
|
162
|
+
onFlushError?: (error: unknown) => void;
|
|
163
|
+
};
|
|
164
|
+
|
|
125
165
|
export type ReactNativeActionEvent = {
|
|
126
166
|
id: string;
|
|
127
167
|
timestamp: string;
|
|
@@ -278,8 +318,11 @@ export type LogBrewNativeActions = {
|
|
|
278
318
|
log(id: string, timestamp: string, attributes: LogAttributes): void;
|
|
279
319
|
span(id: string, timestamp: string, attributes: SpanAttributes): void;
|
|
280
320
|
action(id: string, timestamp: string, attributes: ActionAttributes): void;
|
|
281
|
-
flush(transport
|
|
282
|
-
shutdown(transport
|
|
321
|
+
flush(transport?: Transport): Promise<TransportResponse>;
|
|
322
|
+
shutdown(transport?: Transport): Promise<TransportResponse>;
|
|
323
|
+
deliveryHealth(): DeliveryHealthSnapshot;
|
|
324
|
+
droppedEvents(): number;
|
|
325
|
+
pendingBytes(): number;
|
|
283
326
|
previewJson(): string;
|
|
284
327
|
pendingEvents(): number;
|
|
285
328
|
trace?: ReactNativeTraceContext;
|
|
@@ -295,6 +338,9 @@ export type LogBrewNativeActions = {
|
|
|
295
338
|
export declare function createLogBrewReactNativeClient(
|
|
296
339
|
config: CreateLogBrewReactNativeClientConfig
|
|
297
340
|
): LogBrewClient;
|
|
341
|
+
export declare function createReactNativeFetchTransport(
|
|
342
|
+
config?: ReactNativeFetchTransportConfig
|
|
343
|
+
): Transport;
|
|
298
344
|
export declare function createReactNativeTraceparent(config?: ReactNativeTraceparentConfig): string;
|
|
299
345
|
export declare function createReactNativeTraceContext(
|
|
300
346
|
config?: ReactNativeTraceContextConfig
|
|
@@ -378,7 +424,7 @@ export declare function captureReactNativeError(
|
|
|
378
424
|
export declare function createAppStateListener(
|
|
379
425
|
client: LogBrewClient,
|
|
380
426
|
appState: ReactNativeAppStateLike,
|
|
381
|
-
options?:
|
|
427
|
+
options?: CreateAppStateListenerOptions
|
|
382
428
|
): () => void;
|
|
383
429
|
export declare function LogBrewNativeProvider(props: LogBrewNativeProviderProps): React.ReactElement;
|
|
384
430
|
export declare function useLogBrewNative(): LogBrewNativeContextValue;
|
|
@@ -413,7 +459,7 @@ export declare function captureDefaultReactNativeError(
|
|
|
413
459
|
): ReactNativeErrorEvent;
|
|
414
460
|
export declare function createDefaultAppStateListener(
|
|
415
461
|
client: LogBrewClient,
|
|
416
|
-
options?: Omit<
|
|
462
|
+
options?: Omit<CreateAppStateListenerOptions, "appState">
|
|
417
463
|
): () => void;
|
|
418
464
|
|
|
419
465
|
declare const defaultExport: {
|
|
@@ -429,6 +475,7 @@ declare const defaultExport: {
|
|
|
429
475
|
createAppStateListener: typeof createAppStateListener;
|
|
430
476
|
createLogBrewReactNativeClient: typeof createLogBrewReactNativeClient;
|
|
431
477
|
createReactNavigationSpanListener: typeof createReactNavigationSpanListener;
|
|
478
|
+
createReactNativeFetchTransport: typeof createReactNativeFetchTransport;
|
|
432
479
|
createReactNativeSpanAttributes: typeof createReactNativeSpanAttributes;
|
|
433
480
|
createReactNativeTraceContext: typeof createReactNativeTraceContext;
|
|
434
481
|
createReactNativeTraceHeaders: typeof createReactNativeTraceHeaders;
|
package/index.js
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
createTraceparent,
|
|
5
5
|
LogBrewClient,
|
|
6
6
|
parseTraceparent,
|
|
7
|
-
SdkError
|
|
7
|
+
SdkError,
|
|
8
|
+
TransportError
|
|
8
9
|
} from "@logbrew/sdk";
|
|
9
10
|
import {
|
|
10
11
|
runtimeReactNativeDebugIdMap,
|
|
@@ -14,22 +15,83 @@ import {
|
|
|
14
15
|
|
|
15
16
|
const DEFAULT_SDK_NAME = "logbrew-react-native";
|
|
16
17
|
const DEFAULT_SDK_VERSION = "0.1.0";
|
|
18
|
+
const DEFAULT_ENDPOINT = "https://api.logbrew.co/v1/events";
|
|
19
|
+
const BACKGROUND_APP_STATES = new Set(["background", "inactive"]);
|
|
17
20
|
const LogBrewNativeContext = React.createContext(null);
|
|
18
21
|
const activeTraceScopes = [];
|
|
19
22
|
let nextTraceScopeId = 0;
|
|
20
23
|
|
|
21
24
|
export function createLogBrewReactNativeClient({
|
|
25
|
+
automaticDelivery,
|
|
22
26
|
apiKey,
|
|
23
27
|
clientKey,
|
|
28
|
+
deliveryIntervalMs,
|
|
29
|
+
deliveryQueueThreshold,
|
|
30
|
+
maxBatchBytes,
|
|
31
|
+
maxBatchEvents,
|
|
32
|
+
maxQueueBytes,
|
|
33
|
+
maxQueueSize,
|
|
34
|
+
onEventDropped,
|
|
24
35
|
sdkName = DEFAULT_SDK_NAME,
|
|
25
36
|
sdkVersion = DEFAULT_SDK_VERSION,
|
|
26
|
-
maxRetries = 2
|
|
37
|
+
maxRetries = 2,
|
|
38
|
+
transport
|
|
27
39
|
}) {
|
|
28
40
|
const authKey = clientKey ?? apiKey;
|
|
29
41
|
if (!authKey) {
|
|
30
42
|
throw new SdkError("configuration_error", "createLogBrewReactNativeClient requires clientKey or apiKey");
|
|
31
43
|
}
|
|
32
|
-
return LogBrewClient.create({
|
|
44
|
+
return LogBrewClient.create({
|
|
45
|
+
apiKey: authKey,
|
|
46
|
+
automaticDelivery,
|
|
47
|
+
deliveryIntervalMs,
|
|
48
|
+
deliveryQueueThreshold,
|
|
49
|
+
maxBatchBytes,
|
|
50
|
+
maxBatchEvents,
|
|
51
|
+
maxQueueBytes,
|
|
52
|
+
maxQueueSize,
|
|
53
|
+
maxRetries,
|
|
54
|
+
onEventDropped,
|
|
55
|
+
sdkName,
|
|
56
|
+
sdkVersion,
|
|
57
|
+
transport
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createReactNativeFetchTransport({
|
|
62
|
+
endpoint = DEFAULT_ENDPOINT,
|
|
63
|
+
fetchImpl = defaultFetch(),
|
|
64
|
+
headers = {}
|
|
65
|
+
} = {}) {
|
|
66
|
+
validateDeliveryEndpoint(endpoint);
|
|
67
|
+
if (typeof fetchImpl !== "function") {
|
|
68
|
+
throw new SdkError("configuration_error", "createReactNativeFetchTransport requires fetch");
|
|
69
|
+
}
|
|
70
|
+
if (!headers || Array.isArray(headers) || typeof headers !== "object") {
|
|
71
|
+
throw new SdkError("configuration_error", "createReactNativeFetchTransport headers must be an object");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
async send(apiKey, body) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetchImpl(endpoint, {
|
|
78
|
+
body,
|
|
79
|
+
headers: {
|
|
80
|
+
...headers,
|
|
81
|
+
authorization: `Bearer ${apiKey}`,
|
|
82
|
+
"content-type": "application/json"
|
|
83
|
+
},
|
|
84
|
+
method: "POST"
|
|
85
|
+
});
|
|
86
|
+
const retryAfterMs = retryAfterMsFromHeaders(response?.headers);
|
|
87
|
+
return retryAfterMs === undefined
|
|
88
|
+
? { statusCode: response?.status, attempts: 1 }
|
|
89
|
+
: { statusCode: response?.status, attempts: 1, retryAfterMs };
|
|
90
|
+
} catch {
|
|
91
|
+
throw TransportError.network("LogBrew React Native delivery failed");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
33
95
|
}
|
|
34
96
|
|
|
35
97
|
export function createReactNativeTraceparent({ randomValues = defaultRandomValues, spanId, traceFlags = "01", traceId } = {}) {
|
|
@@ -563,12 +625,34 @@ export function createAppStateListener(client, appState, options = {}) {
|
|
|
563
625
|
if (!appState || typeof appState.addEventListener !== "function") {
|
|
564
626
|
throw new SdkError("configuration_error", "createAppStateListener requires AppState.addEventListener");
|
|
565
627
|
}
|
|
628
|
+
const {
|
|
629
|
+
flushOnBackground = false,
|
|
630
|
+
onFlushError,
|
|
631
|
+
...captureOptions
|
|
632
|
+
} = options;
|
|
633
|
+
if (typeof flushOnBackground !== "boolean") {
|
|
634
|
+
throw new SdkError("configuration_error", "createAppStateListener flushOnBackground must be a boolean");
|
|
635
|
+
}
|
|
636
|
+
if (onFlushError !== undefined && typeof onFlushError !== "function") {
|
|
637
|
+
throw new SdkError("configuration_error", "createAppStateListener onFlushError must be a function");
|
|
638
|
+
}
|
|
566
639
|
|
|
567
640
|
const subscription = appState.addEventListener("change", (nextState) => {
|
|
568
641
|
captureAppStateChange(client, nextState, {
|
|
569
|
-
...
|
|
642
|
+
...captureOptions,
|
|
570
643
|
appState
|
|
571
644
|
});
|
|
645
|
+
if (flushOnBackground && BACKGROUND_APP_STATES.has(nextState)) {
|
|
646
|
+
void client.flush().catch((error) => {
|
|
647
|
+
if (typeof onFlushError === "function") {
|
|
648
|
+
try {
|
|
649
|
+
onFlushError(error);
|
|
650
|
+
} catch {
|
|
651
|
+
// Delivery diagnostics must not interrupt the app-state callback.
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
}
|
|
572
656
|
});
|
|
573
657
|
|
|
574
658
|
return subscriptionRemover(subscription);
|
|
@@ -605,6 +689,9 @@ export function useLogBrewNativeActions() {
|
|
|
605
689
|
action: (id, timestamp, attributes) => client.action(id, timestamp, attributesWithTrace(attributes, trace)),
|
|
606
690
|
flush: client.flush.bind(client),
|
|
607
691
|
shutdown: client.shutdown.bind(client),
|
|
692
|
+
deliveryHealth: client.deliveryHealth.bind(client),
|
|
693
|
+
droppedEvents: client.droppedEvents.bind(client),
|
|
694
|
+
pendingBytes: client.pendingBytes.bind(client),
|
|
608
695
|
previewJson: client.previewJson.bind(client),
|
|
609
696
|
pendingEvents: client.pendingEvents.bind(client),
|
|
610
697
|
trace,
|
|
@@ -688,6 +775,44 @@ function errorMessage(error) {
|
|
|
688
775
|
return String(error ?? "unknown error");
|
|
689
776
|
}
|
|
690
777
|
|
|
778
|
+
function validateDeliveryEndpoint(endpoint) {
|
|
779
|
+
if (typeof endpoint !== "string" || endpoint.trim() === "") {
|
|
780
|
+
throw new SdkError("configuration_error", "createReactNativeFetchTransport requires a non-empty endpoint");
|
|
781
|
+
}
|
|
782
|
+
let parsed;
|
|
783
|
+
try {
|
|
784
|
+
parsed = new URL(endpoint);
|
|
785
|
+
} catch {
|
|
786
|
+
throw new SdkError("configuration_error", "createReactNativeFetchTransport endpoint must be an absolute URL");
|
|
787
|
+
}
|
|
788
|
+
if (parsed.protocol !== "https:") {
|
|
789
|
+
throw new SdkError("configuration_error", "createReactNativeFetchTransport endpoint must use HTTPS");
|
|
790
|
+
}
|
|
791
|
+
if (endpoint !== `${parsed.origin}${parsed.pathname}`) {
|
|
792
|
+
throw new SdkError("configuration_error", "createReactNativeFetchTransport endpoint must be a plain HTTPS path");
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function retryAfterMsFromHeaders(headers) {
|
|
797
|
+
if (!headers || typeof headers.get !== "function") {
|
|
798
|
+
return undefined;
|
|
799
|
+
}
|
|
800
|
+
return retryAfterMsFromHeader(headers.get("retry-after"));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function retryAfterMsFromHeader(value, now = Date.now()) {
|
|
804
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
805
|
+
return undefined;
|
|
806
|
+
}
|
|
807
|
+
const trimmed = value.trim();
|
|
808
|
+
if (/^\d+$/u.test(trimmed)) {
|
|
809
|
+
const milliseconds = Number(trimmed) * 1000;
|
|
810
|
+
return Number.isSafeInteger(milliseconds) ? milliseconds : undefined;
|
|
811
|
+
}
|
|
812
|
+
const timestamp = Date.parse(trimmed);
|
|
813
|
+
return Number.isFinite(timestamp) ? Math.max(0, timestamp - now) : undefined;
|
|
814
|
+
}
|
|
815
|
+
|
|
691
816
|
function defaultErrorEventId({ message, screen }) {
|
|
692
817
|
return `evt_native_error_${slugify(`${screen ?? "app"}_${message}`)}`;
|
|
693
818
|
}
|
|
@@ -966,7 +1091,8 @@ export default {
|
|
|
966
1091
|
LogBrewNativeProvider, captureAppStateChange, captureReactNativeAction, captureReactNativeError,
|
|
967
1092
|
captureReactNativeNetwork, captureReactNativeNavigationSpan, captureReactNativeResourceSpan, captureScreenView,
|
|
968
1093
|
bindLogBrewTrace, createAppStateListener, createLogBrewReactNativeClient, createReactNavigationSpanListener,
|
|
969
|
-
createReactNativeSpanAttributes, createReactNativeTraceContext,
|
|
1094
|
+
createReactNativeFetchTransport, createReactNativeSpanAttributes, createReactNativeTraceContext,
|
|
1095
|
+
createReactNativeTraceHeaders, createReactNativeActionEvent,
|
|
970
1096
|
createReactNativeErrorEvent, createReactNativeNetworkEvent, createReactNativeNavigationSpanEvent,
|
|
971
1097
|
createReactNativeResourceSpanEvent, createReactNativeTraceparent, createTraceparentFetch, getActiveLogBrewTrace,
|
|
972
1098
|
getReactNativeContext, getReactNativeTraceMetadata, shouldPropagateTraceparent, useLogBrewNative,
|
package/metro.cjs
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require("node:crypto");
|
|
4
4
|
const { Buffer } = require("node:buffer");
|
|
5
|
+
const { createRequire } = require("node:module");
|
|
6
|
+
const path = require("node:path");
|
|
5
7
|
|
|
6
8
|
const DEBUG_ID_PLACEHOLDER = "__LOGBREW_REACT_NATIVE_DEBUG_ID__";
|
|
7
9
|
const DEBUG_ID_MODULE_PATH = "__logbrew_debug_id__";
|
|
8
10
|
const DEBUG_ID_REGISTRY_NAME = "@logbrew/react-native/debug-ids";
|
|
9
11
|
const DEBUG_ID_KEYS = ["debug_id", "debugId", "debugID", "x_debug_id"];
|
|
10
12
|
const DEBUG_ID_COMMENT_RE = /(?:\/\/[#@]|\/\*[#@])\s*debugId=[^\r\n]*/iu;
|
|
13
|
+
const DEBUG_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
11
14
|
const SOURCE_MAPPING_COMMENT_RE = /(?:\/\/[#@]|\/\*[#@])\s*sourceMappingURL=[^\r\n]*/giu;
|
|
12
15
|
const WRAPPED_SERIALIZER = Symbol.for("@logbrew/react-native/metro-serializer");
|
|
13
16
|
|
|
@@ -35,8 +38,8 @@ function countLines(source) {
|
|
|
35
38
|
return source === "" ? 0 : source.split("\n").length;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
function createDebugIdModule() {
|
|
39
|
-
const code = runtimeDebugIdSnippet(
|
|
41
|
+
function createDebugIdModule(debugId = DEBUG_ID_PLACEHOLDER) {
|
|
42
|
+
const code = runtimeDebugIdSnippet(debugId);
|
|
40
43
|
return {
|
|
41
44
|
dependencies: new Map(),
|
|
42
45
|
getSource: () => Buffer.from(code),
|
|
@@ -55,14 +58,14 @@ function createDebugIdModule() {
|
|
|
55
58
|
};
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
function prependDebugIdModule(preModules) {
|
|
61
|
+
function prependDebugIdModule(preModules, debugId = DEBUG_ID_PLACEHOLDER) {
|
|
59
62
|
if (!Array.isArray(preModules)) {
|
|
60
63
|
throw configurationError("LogBrew Metro serializer expected preModules to be an array");
|
|
61
64
|
}
|
|
62
65
|
if (preModules.some((module) => module?.path === DEBUG_ID_MODULE_PATH)) {
|
|
63
66
|
return preModules;
|
|
64
67
|
}
|
|
65
|
-
const debugIdModule = createDebugIdModule();
|
|
68
|
+
const debugIdModule = createDebugIdModule(debugId);
|
|
66
69
|
if (preModules[0]?.path === "__prelude__") {
|
|
67
70
|
return [preModules[0], debugIdModule, ...preModules.slice(1)];
|
|
68
71
|
}
|
|
@@ -130,7 +133,12 @@ function sourceWithDebugId(source, debugId) {
|
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
function productionResult(result) {
|
|
133
|
-
if (
|
|
136
|
+
if (Array.isArray(result)) {
|
|
137
|
+
throw configurationError(
|
|
138
|
+
"LogBrew Metro received Expo static assets; use getLogBrewExpoConfig instead of withLogBrewMetroConfig for Expo projects",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (!result || typeof result !== "object") {
|
|
134
142
|
throw configurationError("LogBrew Metro production serializer must return { code, map }");
|
|
135
143
|
}
|
|
136
144
|
if (typeof result.code !== "string") {
|
|
@@ -156,6 +164,81 @@ function productionResult(result) {
|
|
|
156
164
|
};
|
|
157
165
|
}
|
|
158
166
|
|
|
167
|
+
function requireExpoPluginOptions(options) {
|
|
168
|
+
requireOptions(options);
|
|
169
|
+
if (
|
|
170
|
+
options.unstable_beforeAssetSerializationPlugins !== undefined &&
|
|
171
|
+
(!Array.isArray(options.unstable_beforeAssetSerializationPlugins) ||
|
|
172
|
+
options.unstable_beforeAssetSerializationPlugins.some((plugin) => typeof plugin !== "function"))
|
|
173
|
+
) {
|
|
174
|
+
throw configurationError(
|
|
175
|
+
"LogBrew Expo option unstable_beforeAssetSerializationPlugins must be an array of functions",
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (options.getDefaultConfig !== undefined && typeof options.getDefaultConfig !== "function") {
|
|
179
|
+
throw configurationError("LogBrew Expo option getDefaultConfig must be a function");
|
|
180
|
+
}
|
|
181
|
+
return options;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createLogBrewExpoDebugIdPlugin(options = {}) {
|
|
185
|
+
requireOptions(options);
|
|
186
|
+
return (input) => {
|
|
187
|
+
if (!input || Array.isArray(input) || typeof input !== "object") {
|
|
188
|
+
throw configurationError("LogBrew Expo Debug ID plugin requires a serialization input object");
|
|
189
|
+
}
|
|
190
|
+
const preModules = input.premodules;
|
|
191
|
+
if (!Array.isArray(preModules)) {
|
|
192
|
+
throw configurationError("LogBrew Expo Debug ID plugin expected premodules to be an array");
|
|
193
|
+
}
|
|
194
|
+
if (options.enabled === false || input.debugId === undefined || input.debugId === null) {
|
|
195
|
+
return preModules;
|
|
196
|
+
}
|
|
197
|
+
if (typeof input.debugId !== "string" || !DEBUG_ID_RE.test(input.debugId)) {
|
|
198
|
+
throw configurationError("LogBrew Expo Debug ID plugin requires a valid Expo Debug ID");
|
|
199
|
+
}
|
|
200
|
+
return prependDebugIdModule(preModules, input.debugId.toLowerCase());
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function loadExpoGetDefaultConfig(projectRoot) {
|
|
205
|
+
let expoMetroConfig;
|
|
206
|
+
try {
|
|
207
|
+
const projectRequire = createRequire(path.join(projectRoot, "package.json"));
|
|
208
|
+
expoMetroConfig = projectRequire("expo/metro-config");
|
|
209
|
+
} catch (error) {
|
|
210
|
+
throw configurationError(
|
|
211
|
+
"LogBrew could not load expo/metro-config from the app; install a supported Expo SDK or pass getDefaultConfig",
|
|
212
|
+
{ cause: error },
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (typeof expoMetroConfig?.getDefaultConfig !== "function") {
|
|
216
|
+
throw configurationError("LogBrew could not resolve getDefaultConfig from expo/metro-config");
|
|
217
|
+
}
|
|
218
|
+
return expoMetroConfig.getDefaultConfig;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getLogBrewExpoConfig(projectRoot, options = {}) {
|
|
222
|
+
if (typeof projectRoot !== "string" || projectRoot.trim() === "") {
|
|
223
|
+
throw configurationError("getLogBrewExpoConfig requires a non-empty Expo project root");
|
|
224
|
+
}
|
|
225
|
+
requireExpoPluginOptions(options);
|
|
226
|
+
const root = path.resolve(projectRoot);
|
|
227
|
+
const {
|
|
228
|
+
enabled = true,
|
|
229
|
+
getDefaultConfig = loadExpoGetDefaultConfig(root),
|
|
230
|
+
unstable_beforeAssetSerializationPlugins = [],
|
|
231
|
+
...expoOptions
|
|
232
|
+
} = options;
|
|
233
|
+
const plugins = enabled
|
|
234
|
+
? [...unstable_beforeAssetSerializationPlugins, createLogBrewExpoDebugIdPlugin()]
|
|
235
|
+
: [...unstable_beforeAssetSerializationPlugins];
|
|
236
|
+
return getDefaultConfig(root, {
|
|
237
|
+
...expoOptions,
|
|
238
|
+
unstable_beforeAssetSerializationPlugins: plugins,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
159
242
|
function requireMetroModule(privatePath, sourcePath) {
|
|
160
243
|
try {
|
|
161
244
|
return require(privatePath);
|
|
@@ -305,6 +388,7 @@ function withLogBrewMetroConfig(config, options = {}) {
|
|
|
305
388
|
|
|
306
389
|
module.exports = {
|
|
307
390
|
createLogBrewMetroSerializer,
|
|
391
|
+
getLogBrewExpoConfig,
|
|
308
392
|
withLogBrewMetroConfig,
|
|
309
393
|
default: withLogBrewMetroConfig,
|
|
310
394
|
};
|
package/metro.d.cts
CHANGED
|
@@ -14,21 +14,41 @@ export type LogBrewMetroSerializer<TModule = unknown, TGraph = unknown, TOptions
|
|
|
14
14
|
export type LogBrewMetroConfig = {
|
|
15
15
|
serializer?: {
|
|
16
16
|
customSerializer?: unknown;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
[key: string]: unknown;
|
|
20
|
-
};
|
|
17
|
+
} | Record<string, unknown>;
|
|
18
|
+
} | Record<string, unknown>;
|
|
21
19
|
|
|
22
20
|
export type LogBrewMetroConfigOptions = {
|
|
23
21
|
enabled?: boolean;
|
|
24
22
|
};
|
|
25
23
|
|
|
24
|
+
export type LogBrewExpoSerializationInput<TModule = unknown, TGraph = unknown> = {
|
|
25
|
+
debugId?: string;
|
|
26
|
+
graph: TGraph;
|
|
27
|
+
premodules: TModule[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type LogBrewExpoDebugIdPlugin<TModule = unknown, TGraph = unknown> = (
|
|
31
|
+
input: LogBrewExpoSerializationInput<TModule, TGraph>,
|
|
32
|
+
) => TModule[];
|
|
33
|
+
|
|
34
|
+
export type LogBrewExpoConfigOptions<TConfig extends LogBrewMetroConfig = LogBrewMetroConfig> = {
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
getDefaultConfig?: (...args: never[]) => TConfig;
|
|
37
|
+
unstable_beforeAssetSerializationPlugins?: LogBrewExpoDebugIdPlugin[];
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
};
|
|
40
|
+
|
|
26
41
|
export declare function createLogBrewMetroSerializer<TModule, TGraph, TOptions>(
|
|
27
42
|
customSerializer: LogBrewMetroSerializer<TModule, TGraph, TOptions>,
|
|
28
43
|
): LogBrewMetroSerializer<TModule, TGraph, TOptions>;
|
|
29
44
|
|
|
30
45
|
export declare function createLogBrewMetroSerializer(customSerializer?: null): LogBrewMetroSerializer;
|
|
31
46
|
|
|
47
|
+
export declare function getLogBrewExpoConfig<TConfig extends LogBrewMetroConfig = LogBrewMetroConfig>(
|
|
48
|
+
projectRoot: string,
|
|
49
|
+
options?: LogBrewExpoConfigOptions<TConfig>,
|
|
50
|
+
): TConfig;
|
|
51
|
+
|
|
32
52
|
export declare function withLogBrewMetroConfig<T extends LogBrewMetroConfig>(
|
|
33
53
|
config: T,
|
|
34
54
|
options?: LogBrewMetroConfigOptions,
|
package/metro.d.ts
CHANGED
|
@@ -14,21 +14,41 @@ export type LogBrewMetroSerializer<TModule = unknown, TGraph = unknown, TOptions
|
|
|
14
14
|
export type LogBrewMetroConfig = {
|
|
15
15
|
serializer?: {
|
|
16
16
|
customSerializer?: unknown;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
[key: string]: unknown;
|
|
20
|
-
};
|
|
17
|
+
} | Record<string, unknown>;
|
|
18
|
+
} | Record<string, unknown>;
|
|
21
19
|
|
|
22
20
|
export type LogBrewMetroConfigOptions = {
|
|
23
21
|
enabled?: boolean;
|
|
24
22
|
};
|
|
25
23
|
|
|
24
|
+
export type LogBrewExpoSerializationInput<TModule = unknown, TGraph = unknown> = {
|
|
25
|
+
debugId?: string;
|
|
26
|
+
graph: TGraph;
|
|
27
|
+
premodules: TModule[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type LogBrewExpoDebugIdPlugin<TModule = unknown, TGraph = unknown> = (
|
|
31
|
+
input: LogBrewExpoSerializationInput<TModule, TGraph>,
|
|
32
|
+
) => TModule[];
|
|
33
|
+
|
|
34
|
+
export type LogBrewExpoConfigOptions<TConfig extends LogBrewMetroConfig = LogBrewMetroConfig> = {
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
getDefaultConfig?: (...args: never[]) => TConfig;
|
|
37
|
+
unstable_beforeAssetSerializationPlugins?: LogBrewExpoDebugIdPlugin[];
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
};
|
|
40
|
+
|
|
26
41
|
export declare function createLogBrewMetroSerializer<TModule, TGraph, TOptions>(
|
|
27
42
|
customSerializer: LogBrewMetroSerializer<TModule, TGraph, TOptions>,
|
|
28
43
|
): LogBrewMetroSerializer<TModule, TGraph, TOptions>;
|
|
29
44
|
|
|
30
45
|
export declare function createLogBrewMetroSerializer(customSerializer?: null): LogBrewMetroSerializer;
|
|
31
46
|
|
|
47
|
+
export declare function getLogBrewExpoConfig<TConfig extends LogBrewMetroConfig = LogBrewMetroConfig>(
|
|
48
|
+
projectRoot: string,
|
|
49
|
+
options?: LogBrewExpoConfigOptions<TConfig>,
|
|
50
|
+
): TConfig;
|
|
51
|
+
|
|
32
52
|
export declare function withLogBrewMetroConfig<T extends LogBrewMetroConfig>(
|
|
33
53
|
config: T,
|
|
34
54
|
options?: LogBrewMetroConfigOptions,
|
package/metro.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import metro from "./metro.cjs";
|
|
2
2
|
|
|
3
3
|
export const createLogBrewMetroSerializer = metro.createLogBrewMetroSerializer;
|
|
4
|
+
export const getLogBrewExpoConfig = metro.getLogBrewExpoConfig;
|
|
4
5
|
export const withLogBrewMetroConfig = metro.withLogBrewMetroConfig;
|
|
5
6
|
|
|
6
7
|
export default withLogBrewMetroConfig;
|