@sazabi/react-native 0.0.0-stage → 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/dist/chunk-c8j7sr2j.js +5 -0
- package/dist/chunk-c8j7sr2j.js.map +24 -0
- package/dist/client-sdk-core/attributes.d.ts +8 -0
- package/dist/client-sdk-core/clock.d.ts +6 -0
- package/dist/client-sdk-core/console.d.ts +23 -0
- package/dist/client-sdk-core/index.d.ts +21 -0
- package/dist/client-sdk-core/intake-url.d.ts +38 -0
- package/dist/client-sdk-core/network-capture.d.ts +90 -0
- package/dist/client-sdk-core/patch-marker.d.ts +8 -0
- package/dist/client-sdk-core/pointer.d.ts +73 -0
- package/dist/client-sdk-core/random.d.ts +11 -0
- package/dist/client-sdk-core/sdk-health.d.ts +37 -0
- package/dist/client-sdk-core/session.d.ts +44 -0
- package/dist/client-sdk-core/trace-context.d.ts +33 -0
- package/dist/client-sdk-core/transport.d.ts +142 -0
- package/dist/client-sdk-core/types.d.ts +76 -0
- package/dist/client-sdk-core/url.d.ts +30 -0
- package/dist/errors.d.ts +46 -0
- package/dist/host.d.ts +30 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +18 -0
- package/dist/interactions.d.ts +40 -0
- package/dist/lifecycle.d.ts +16 -0
- package/dist/navigation.d.ts +24 -0
- package/dist/network.d.ts +12 -0
- package/dist/outbox.d.ts +53 -0
- package/dist/process-state.d.ts +49 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +4 -0
- package/dist/register.js.map +10 -0
- package/dist/storage.d.ts +42 -0
- package/dist/types.d.ts +108 -0
- package/dist/version.d.ts +4 -0
- package/package.json +49 -4
- package/README.md +0 -3
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type RandomBytes } from "./random.js";
|
|
2
|
+
import type { ClientEvent } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pointer episodes: one row per contact from down to up, for mouse, touch,
|
|
5
|
+
* and pen alike. A binding feeds raw contacts in (DOM pointer events in the
|
|
6
|
+
* browser, the responder's touch arrays on mobile) and the tracker emits the
|
|
7
|
+
* episode rows plus the derived `click` and `rage_click` rows. Movement is
|
|
8
|
+
* sampled, never streamed: a position sample every `sampleIntervalMs` is
|
|
9
|
+
* packed into the row, and a contact that outlives `partialIntervalMs` emits
|
|
10
|
+
* a partial row at that cadence so a long scroll is not a blind spot.
|
|
11
|
+
*/
|
|
12
|
+
export type PointerType = "mouse" | "touch" | "pen";
|
|
13
|
+
export interface PointerTarget {
|
|
14
|
+
/** Developer-authored name for the widget under the pointer, if any. */
|
|
15
|
+
name?: string;
|
|
16
|
+
/** Stable-ish identifier: a selector on the web, a component path on mobile. */
|
|
17
|
+
id?: string;
|
|
18
|
+
/** Element tag or component kind. */
|
|
19
|
+
tag?: string;
|
|
20
|
+
/** Whether a tap here is on something that should respond. */
|
|
21
|
+
interactive: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface PointerContact {
|
|
24
|
+
id: number | string;
|
|
25
|
+
type: PointerType;
|
|
26
|
+
primary: boolean;
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
pressure?: number;
|
|
30
|
+
/**
|
|
31
|
+
* The button that started a mouse contact (DOM `button`: 0 primary, 1
|
|
32
|
+
* auxiliary, 2 secondary). Absent for touch and pen. Only a primary-button
|
|
33
|
+
* contact can derive a click: a short right or middle press is an episode
|
|
34
|
+
* without a tap, matching the host's `contextmenu` and `auxclick`.
|
|
35
|
+
*/
|
|
36
|
+
button?: number;
|
|
37
|
+
target?: PointerTarget;
|
|
38
|
+
}
|
|
39
|
+
export interface DerivedTap {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
target?: PointerTarget;
|
|
43
|
+
/** Absent for a tap no contact produced (keyboard activation, script). */
|
|
44
|
+
gestureId?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface PointerTrackerOptions {
|
|
47
|
+
emit(event: ClientEvent): void;
|
|
48
|
+
randomBytes: RandomBytes;
|
|
49
|
+
/** Spacing of packed position samples. Default 1000. */
|
|
50
|
+
sampleIntervalMs?: number;
|
|
51
|
+
/** Cadence of partial rows for long contacts. Default 10000. */
|
|
52
|
+
partialIntervalMs?: number;
|
|
53
|
+
/** Every derived tap, so a binding can run its own dead-tap detection. */
|
|
54
|
+
onTap?(tap: DerivedTap): void;
|
|
55
|
+
}
|
|
56
|
+
export interface PointerTracker {
|
|
57
|
+
down(contact: PointerContact): void;
|
|
58
|
+
move(id: number | string, x: number, y: number, pressure?: number): void;
|
|
59
|
+
up(id: number | string, x: number, y: number): void;
|
|
60
|
+
cancel(id: number | string): void;
|
|
61
|
+
/**
|
|
62
|
+
* A click that no pointer contact produced: keyboard activation or a
|
|
63
|
+
* script calling `click()`. Emits the `click` row and runs the same rage
|
|
64
|
+
* and dead-tap derivation, with no episode behind it.
|
|
65
|
+
*/
|
|
66
|
+
tap(tap: {
|
|
67
|
+
x: number;
|
|
68
|
+
y: number;
|
|
69
|
+
target?: PointerTarget;
|
|
70
|
+
}): void;
|
|
71
|
+
teardown(): void;
|
|
72
|
+
}
|
|
73
|
+
export declare const createPointerTracker: (options: PointerTrackerOptions) => PointerTracker;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type RandomBytes = (length: number) => Uint8Array;
|
|
2
|
+
/**
|
|
3
|
+
* Random bytes for session, trace, and gesture ids. Web Crypto where the host
|
|
4
|
+
* has it; a seeded fallback where it does not (React Native without a
|
|
5
|
+
* `crypto.getRandomValues` polyfill). The ids need uniqueness, not secrecy,
|
|
6
|
+
* so the fallback is acceptable there and is never used where crypto exists.
|
|
7
|
+
*/
|
|
8
|
+
export declare const createRandomBytes: () => RandomBytes;
|
|
9
|
+
export declare const toHex: (bytes: Uint8Array) => string;
|
|
10
|
+
export declare const randomHex: (randomBytes: RandomBytes, byteLength: number) => string;
|
|
11
|
+
export declare const newPrefixedId: (randomBytes: RandomBytes, prefix: string) => string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ClientEvent, EventAttributes } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* What an `sdk_health` row reports: the SDK's own delivery trouble, so a
|
|
4
|
+
* misconfigured or unreachable app is visible to Sazabi before the customer
|
|
5
|
+
* reports it. Each value is a bounded enumeration, never free text.
|
|
6
|
+
*
|
|
7
|
+
* - `intake_rejected`: the intake refused a batch for good (a 4xx other
|
|
8
|
+
* than 408 and 429); `app.sdk.http_status` carries the status.
|
|
9
|
+
* - `intake_unreachable`: uploads failed on the network (a dropped
|
|
10
|
+
* connection, a timeout) and the intake later answered; `app.sdk.attempts`
|
|
11
|
+
* is how many backoff windows the outage spanned (one per failed try,
|
|
12
|
+
* with concurrent uploads failing together counted once).
|
|
13
|
+
* - `outbox_discarded`: stored crash reports were dropped, by the caps or
|
|
14
|
+
* because they were not valid reports; `app.sdk.outbox_discarded` counts
|
|
15
|
+
* what is known lost, and `app.sdk.outbox_unsaved` what a saved queue had
|
|
16
|
+
* left out at the caps (possible losses: their live upload may have
|
|
17
|
+
* finished before the app was killed) (mobile only).
|
|
18
|
+
* - `outbox_persist_failed`: the storage adapter refused a write of the
|
|
19
|
+
* crash outbox or the saved queue (mobile only).
|
|
20
|
+
*/
|
|
21
|
+
export type SdkHealthDiagnostic = "intake_rejected" | "intake_unreachable" | "outbox_discarded" | "outbox_persist_failed";
|
|
22
|
+
export interface SdkHealthReport {
|
|
23
|
+
diagnostic: SdkHealthDiagnostic;
|
|
24
|
+
/** The counters of the condition (`app.sdk.http_status`, `app.sdk.attempts`, ...). */
|
|
25
|
+
attributes?: EventAttributes;
|
|
26
|
+
}
|
|
27
|
+
export type SdkHealthReporter = (report: SdkHealthReport) => void;
|
|
28
|
+
export declare const SDK_DIAGNOSTIC_KEY = "app.sdk.diagnostic";
|
|
29
|
+
/**
|
|
30
|
+
* Turns health reports into `sdk_health` rows, at most one per diagnostic
|
|
31
|
+
* for the life of the reporter (one SDK session). The bound is what keeps
|
|
32
|
+
* the SDK from storming its own intake: a health row the intake refuses is
|
|
33
|
+
* a rejection like any other and is never reported again, and a flapping
|
|
34
|
+
* network produces one row per launch, not one per outage. Never throws
|
|
35
|
+
* into the host.
|
|
36
|
+
*/
|
|
37
|
+
export declare const createSdkHealthReporter: (emit: (event: ClientEvent) => void) => SdkHealthReporter;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type RandomBytes } from "./random.js";
|
|
2
|
+
/**
|
|
3
|
+
* Synchronous key-value access the session machine reads through on every
|
|
4
|
+
* call. A browser binding backs it with `localStorage` (so sibling tabs
|
|
5
|
+
* converge) and `sessionStorage`; a mobile binding backs it with an
|
|
6
|
+
* in-memory mirror hydrated from its storage adapter and written through.
|
|
7
|
+
*/
|
|
8
|
+
export interface SessionStore {
|
|
9
|
+
get(key: string): string | null;
|
|
10
|
+
set(key: string, value: string): void;
|
|
11
|
+
remove(key: string): void;
|
|
12
|
+
}
|
|
13
|
+
/** A store that lives for the process only: the fallback on every host. */
|
|
14
|
+
export declare const createMemoryStore: () => SessionStore;
|
|
15
|
+
export interface SessionManager {
|
|
16
|
+
getSessionId(): string;
|
|
17
|
+
getWindowId(): string;
|
|
18
|
+
getDistinctId(): string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Set the client-asserted identity. Switching from one identity directly
|
|
21
|
+
* to a different one rotates the session and window ids so two users on a
|
|
22
|
+
* shared device never thread into one timeline.
|
|
23
|
+
*/
|
|
24
|
+
setDistinctId(distinctId: string): void;
|
|
25
|
+
/** Logout: clear identity and rotate both session and window ids. */
|
|
26
|
+
reset(): void;
|
|
27
|
+
/** Record user activity; may rotate the session on idle/max-age expiry. */
|
|
28
|
+
touch(): void;
|
|
29
|
+
}
|
|
30
|
+
export interface SessionManagerOptions {
|
|
31
|
+
/** Visit-scoped state: session id and identity. */
|
|
32
|
+
store: SessionStore;
|
|
33
|
+
/** Window-scoped state: one browser tab, or one app launch. */
|
|
34
|
+
windowStore: SessionStore;
|
|
35
|
+
randomBytes: RandomBytes;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Session identity. `session.id` is the visit (last-writer-wins on rotation
|
|
39
|
+
* races between windows; a split session is cosmetic, not data loss).
|
|
40
|
+
* `session.window_id` is this window. `session.distinct_id` is read through
|
|
41
|
+
* the store so identify/reset in one window is visible to siblings on their
|
|
42
|
+
* next event.
|
|
43
|
+
*/
|
|
44
|
+
export declare const createSessionManager: (options: SessionManagerOptions) => SessionManager;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type RandomBytes } from "./random.js";
|
|
2
|
+
export interface RequestTraceContext {
|
|
3
|
+
/** Headers to inject (`traceparent`, and `tracestate` when present). */
|
|
4
|
+
headers: Record<string, string>;
|
|
5
|
+
traceId: string;
|
|
6
|
+
spanId: string;
|
|
7
|
+
}
|
|
8
|
+
export interface TraceContextManager {
|
|
9
|
+
/** Trace context for one outgoing request, minted at request time. */
|
|
10
|
+
forRequest(): RequestTraceContext;
|
|
11
|
+
}
|
|
12
|
+
/** The subset of an OTel SpanContext this module consumes. */
|
|
13
|
+
export interface HostSpanContext {
|
|
14
|
+
traceId: string;
|
|
15
|
+
spanId: string;
|
|
16
|
+
traceFlags: number;
|
|
17
|
+
traceState?: {
|
|
18
|
+
serialize(): string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare const readHostSpanContext: () => HostSpanContext | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* W3C trace context for outgoing requests: hand-rolled header formatting
|
|
24
|
+
* (equivalence-tested against OTel's W3CTraceContextPropagator), zero
|
|
25
|
+
* runtime dependencies. If the host app runs its own OTel and has an active
|
|
26
|
+
* span, that span's context (including its flags and tracestate) is reused
|
|
27
|
+
* so we join the customer's trace instead of forking a new one; otherwise a
|
|
28
|
+
* fresh root is minted with the sampled flag defaulting to off (`-00`).
|
|
29
|
+
*/
|
|
30
|
+
export declare const createTraceContextManager: (options: {
|
|
31
|
+
sampledFlag: boolean;
|
|
32
|
+
randomBytes: RandomBytes;
|
|
33
|
+
}) => TraceContextManager;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { SdkHealthReport } from "./sdk-health.js";
|
|
2
|
+
import type { ClientEvent, EventAttributes, EventSeverity } from "./types.js";
|
|
3
|
+
type OtlpAnyValue = {
|
|
4
|
+
stringValue: string;
|
|
5
|
+
} | {
|
|
6
|
+
intValue: string;
|
|
7
|
+
} | {
|
|
8
|
+
doubleValue: number;
|
|
9
|
+
} | {
|
|
10
|
+
boolValue: boolean;
|
|
11
|
+
};
|
|
12
|
+
interface OtlpKeyValue {
|
|
13
|
+
key: string;
|
|
14
|
+
value: OtlpAnyValue;
|
|
15
|
+
}
|
|
16
|
+
export interface OtlpLogRecord {
|
|
17
|
+
timeUnixNano: string;
|
|
18
|
+
severityNumber: number;
|
|
19
|
+
severityText: EventSeverity;
|
|
20
|
+
body: {
|
|
21
|
+
stringValue: string;
|
|
22
|
+
};
|
|
23
|
+
attributes: OtlpKeyValue[];
|
|
24
|
+
/** OTLP/JSON encodes trace/span ids as hex (unlike proto3 JSON's base64). */
|
|
25
|
+
traceId?: string;
|
|
26
|
+
spanId?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare const encodeLogRecord: (event: ClientEvent, baseAttributes: EventAttributes) => OtlpLogRecord;
|
|
29
|
+
/**
|
|
30
|
+
* The slice of `fetch` the transport uses. Declared structurally so the core
|
|
31
|
+
* needs no DOM lib; every host's real `fetch` satisfies it.
|
|
32
|
+
*/
|
|
33
|
+
export type FetchLike = (url: string, init: {
|
|
34
|
+
method: "POST";
|
|
35
|
+
headers: Record<string, string>;
|
|
36
|
+
body: string;
|
|
37
|
+
keepalive: boolean;
|
|
38
|
+
/** An `AbortSignal` where the host has `AbortController`; aborts a timed-out upload. */
|
|
39
|
+
signal?: unknown;
|
|
40
|
+
}) => Promise<unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* UTF-8 byte length of a string, without `TextEncoder` (Hermes has none).
|
|
43
|
+
* Request budgets are bytes on the wire; a string's `length` counts UTF-16
|
|
44
|
+
* units and undercounts non-ASCII text up to threefold.
|
|
45
|
+
*/
|
|
46
|
+
export declare const utf8Length: (text: string) => number;
|
|
47
|
+
/** Stamped on the first row delivered after events were dropped: how many. */
|
|
48
|
+
export declare const DROPPED_EVENTS_KEY = "app.sdk.dropped_events";
|
|
49
|
+
/**
|
|
50
|
+
* Names the sending SDK on every upload, `<package>/<version>`: the input
|
|
51
|
+
* for the intake gateway's per-SDK request label (a separate change), which
|
|
52
|
+
* can then count a request by SDK even when it rejects it before the key is
|
|
53
|
+
* known. The gateway's CORS allow-list admits it, so a browser preflight
|
|
54
|
+
* passes.
|
|
55
|
+
*/
|
|
56
|
+
export declare const SDK_HEADER = "x-sazabi-sdk";
|
|
57
|
+
export interface TransportOptions {
|
|
58
|
+
/** Full OTLP logs endpoint, e.g. `https://<key>.<region>.intake.<domain>/v1/logs`. */
|
|
59
|
+
url: string;
|
|
60
|
+
publicKey: string;
|
|
61
|
+
resourceAttributes: EventAttributes;
|
|
62
|
+
/** OTLP instrumentation scope: the emitting SDK's name and version. */
|
|
63
|
+
scope: {
|
|
64
|
+
name: string;
|
|
65
|
+
version: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The fetch to deliver with, captured before the SDK's network patch
|
|
69
|
+
* installs, so exporter traffic can never recurse through our own wrapper
|
|
70
|
+
* or appear in the captured event stream.
|
|
71
|
+
*/
|
|
72
|
+
fetchImpl: FetchLike;
|
|
73
|
+
flushIntervalMs: number;
|
|
74
|
+
maxBatchBytes: number;
|
|
75
|
+
maxQueuedEvents: number;
|
|
76
|
+
/** Reports a dropped batch or a full queue; called once per reason. */
|
|
77
|
+
warn?(message: string): void;
|
|
78
|
+
/**
|
|
79
|
+
* The SDK's own delivery health: a batch the intake refused for good
|
|
80
|
+
* (`intake_rejected`, with the status) and uploads that failed on the
|
|
81
|
+
* network before one got through (`intake_unreachable`, with the number
|
|
82
|
+
* of failed tries). The binding turns these into `sdk_health` rows, once
|
|
83
|
+
* per condition per session.
|
|
84
|
+
*/
|
|
85
|
+
onHealth?(report: SdkHealthReport): void;
|
|
86
|
+
/**
|
|
87
|
+
* Called when an upload settles with nothing queued, nothing in flight, and
|
|
88
|
+
* every row of the last `snapshotEnvelopes` delivered or refused for good:
|
|
89
|
+
* a saved row evicted from the full queue, or given up by `shutdown()`
|
|
90
|
+
* while the intake was unreachable, keeps it from firing until the next
|
|
91
|
+
* snapshot, so the host's saved copy stays.
|
|
92
|
+
*/
|
|
93
|
+
onIdle?(): void;
|
|
94
|
+
}
|
|
95
|
+
export interface Transport {
|
|
96
|
+
enqueue(record: OtlpLogRecord): void;
|
|
97
|
+
/**
|
|
98
|
+
* Deliver everything queued. `keepalive: true` is for browser unload paths;
|
|
99
|
+
* each request body must stay inside the browser's ~64 KiB keepalive
|
|
100
|
+
* budget, which enqueue-side chunking guarantees via `maxBatchBytes`. Hosts
|
|
101
|
+
* without keepalive ignore the flag.
|
|
102
|
+
*/
|
|
103
|
+
flush(options?: {
|
|
104
|
+
keepalive?: boolean;
|
|
105
|
+
}): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Encode everything queued into standalone request bodies without sending,
|
|
108
|
+
* leaving the queue empty. The mobile crash outbox persists these and posts
|
|
109
|
+
* them on the next launch; `recordAttributes` are appended to every record
|
|
110
|
+
* (the outbox stamps its replay id there).
|
|
111
|
+
*/
|
|
112
|
+
drainEnvelopes(recordAttributes?: EventAttributes): string[];
|
|
113
|
+
/**
|
|
114
|
+
* Encode everything queued into standalone request bodies and keep it
|
|
115
|
+
* queued. `recordAttributes` are stamped on the queued records themselves,
|
|
116
|
+
* so the live upload and the saved copy carry the same values (the mobile
|
|
117
|
+
* SDK saves its queue this way when the app leaves the foreground). Rows an
|
|
118
|
+
* earlier snapshot saved whose upload is still in flight are included as
|
|
119
|
+
* they were sent: the copy this one replaces held them.
|
|
120
|
+
*/
|
|
121
|
+
snapshotEnvelopes(recordAttributes: EventAttributes): string[];
|
|
122
|
+
/**
|
|
123
|
+
* While held, `enqueue` never flushes on its own, neither on the byte
|
|
124
|
+
* budget nor by scheduling the timer: the mobile crash path holds the
|
|
125
|
+
* transport while it stamps the fatal row and everything buffered before
|
|
126
|
+
* it, then drains the whole batch into the outbox. Without this, a fatal
|
|
127
|
+
* row that tips the queue over the budget would be flushed out of the queue
|
|
128
|
+
* into a request the dying process never completes.
|
|
129
|
+
*/
|
|
130
|
+
holdFlushes(hold: boolean): void;
|
|
131
|
+
/**
|
|
132
|
+
* Deliver a previously drained envelope. Resolves `true` once the envelope
|
|
133
|
+
* is settled: the intake accepted it (2xx) or refused it for good (a 4xx
|
|
134
|
+
* other than 408 and 429, dropped with a warning). Resolves `false` when it
|
|
135
|
+
* is worth sending again (408, 429, 5xx, a network failure, a timeout), and
|
|
136
|
+
* never rejects, so a persisted envelope is deleted only once settled.
|
|
137
|
+
*/
|
|
138
|
+
sendEnvelope(body: string): Promise<boolean>;
|
|
139
|
+
shutdown(): Promise<void>;
|
|
140
|
+
}
|
|
141
|
+
export declare const createTransport: (options: TransportOptions) => Transport;
|
|
142
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export type AttributeValue = string | number | boolean;
|
|
2
|
+
export type EventAttributes = Record<string, AttributeValue | undefined>;
|
|
3
|
+
export type EventSeverity = "DEBUG" | "INFO" | "WARN" | "ERROR";
|
|
4
|
+
/**
|
|
5
|
+
* The shared `app.event_type` vocabulary. Every value means the same thing
|
|
6
|
+
* on every host; a binding that cannot produce one simply never emits it.
|
|
7
|
+
*/
|
|
8
|
+
export type ClientEventType = "navigation" | "pointer" | "click" | "rage_click" | "dead_click" | "input" | "network" | "error" | "custom" | "log" | "lifecycle"
|
|
9
|
+
/** The SDK's own delivery health, one row per condition per session (`sdk-health.ts`). */
|
|
10
|
+
| "sdk_health";
|
|
11
|
+
/** An SDK-internal semantic event, before OTLP encoding. */
|
|
12
|
+
export interface ClientEvent {
|
|
13
|
+
type: ClientEventType;
|
|
14
|
+
body: string;
|
|
15
|
+
severity: EventSeverity;
|
|
16
|
+
timeUnixMs: number;
|
|
17
|
+
/** Set on network events when trace context was injected into the request. */
|
|
18
|
+
traceId?: string;
|
|
19
|
+
spanId?: string;
|
|
20
|
+
attributes: EventAttributes;
|
|
21
|
+
}
|
|
22
|
+
export type ConsoleCaptureLevel = "error" | "warn" | "info" | "debug";
|
|
23
|
+
export interface ConsoleCaptureConfig {
|
|
24
|
+
/** Mirror console output into the event stream. Default true. */
|
|
25
|
+
capture?: boolean;
|
|
26
|
+
/** Console levels to mirror. Default ["error", "warn"]. */
|
|
27
|
+
levels?: ConsoleCaptureLevel[];
|
|
28
|
+
}
|
|
29
|
+
export interface PointerCaptureConfig {
|
|
30
|
+
/** Emit one `pointer` episode per contact, plus derived taps. Default true. */
|
|
31
|
+
capture?: boolean;
|
|
32
|
+
/** Spacing of the position samples packed into an episode. Default 1000. */
|
|
33
|
+
sampleIntervalMs?: number;
|
|
34
|
+
/**
|
|
35
|
+
* A contact open longer than this emits a partial row at this cadence so a
|
|
36
|
+
* long scroll or drag is not a blind spot until release. Default 10000.
|
|
37
|
+
*/
|
|
38
|
+
partialIntervalMs?: number;
|
|
39
|
+
}
|
|
40
|
+
/** Network options every binding shares; a binding may extend the shape. */
|
|
41
|
+
export interface NetworkCaptureConfig {
|
|
42
|
+
/** Emit network events. Default true. */
|
|
43
|
+
capture?: boolean;
|
|
44
|
+
/** Inject W3C `traceparent` into allowlisted requests. Default true. */
|
|
45
|
+
propagateTraceContext?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Response headers probed (in order) for a platform request id to attach
|
|
48
|
+
* as `app.network.request_id`, an exact join key against platform logs.
|
|
49
|
+
*/
|
|
50
|
+
requestIdHeaders?: string[];
|
|
51
|
+
/**
|
|
52
|
+
* Requests eligible for `traceparent` injection. A string is a host
|
|
53
|
+
* (`api.example.com`), a domain with every subdomain (`.example.com`), or
|
|
54
|
+
* an origin with an optional path prefix (`https://api.example.com/v2`); a
|
|
55
|
+
* RegExp is tested against the full URL. An empty list falls back to the
|
|
56
|
+
* binding's default policy (same-origin in the browser, every request on
|
|
57
|
+
* mobile).
|
|
58
|
+
*/
|
|
59
|
+
allowlist?: (string | RegExp)[];
|
|
60
|
+
/**
|
|
61
|
+
* Query parameter names whose values are replaced with `[redacted]` in
|
|
62
|
+
* every recorded URL: `url.full` of requests and navigations, the screen or
|
|
63
|
+
* page URL stamped on each event (`app.screen.url`), and in the browser the
|
|
64
|
+
* referrer (`app.navigation.from`). Beside the default list (token, access_token, refresh_token,
|
|
65
|
+
* id_token, api_key, apikey, key, secret, password, signature, sig,
|
|
66
|
+
* x-amz-signature, x-amz-credential, x-goog-signature, sentry_key,
|
|
67
|
+
* authorization, auth, session, sid, code). Strings match a name exactly,
|
|
68
|
+
* case-insensitively; RegExps are tested against the name.
|
|
69
|
+
*/
|
|
70
|
+
redactQueryParams?: (string | RegExp)[];
|
|
71
|
+
/**
|
|
72
|
+
* Set the sampled flag (`-01`) on SDK-minted trace roots. Default false:
|
|
73
|
+
* the sampled bit steers customers' ParentBased backend samplers.
|
|
74
|
+
*/
|
|
75
|
+
sampledFlag?: boolean;
|
|
76
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Absolute-URL parsing with no `URL` global. React Native's built-in `URL`
|
|
3
|
+
* throws "not implemented" for `hostname`, `pathname`, and `origin`, and the
|
|
4
|
+
* core must work there unpolyfilled, so the few fields the SDKs read are
|
|
5
|
+
* parsed here. Only absolute http(s)-style URLs resolve; a binding resolves
|
|
6
|
+
* relative references against its own base before calling in.
|
|
7
|
+
*/
|
|
8
|
+
export interface ParsedUrl {
|
|
9
|
+
/** e.g. `https:` */
|
|
10
|
+
protocol: string;
|
|
11
|
+
/** host with port, no credentials */
|
|
12
|
+
host: string;
|
|
13
|
+
hostname: string;
|
|
14
|
+
pathname: string;
|
|
15
|
+
search: string;
|
|
16
|
+
hash: string;
|
|
17
|
+
origin: string;
|
|
18
|
+
/** Normalized absolute URL with credentials stripped. */
|
|
19
|
+
href: string;
|
|
20
|
+
}
|
|
21
|
+
export declare const parseAbsoluteUrl: (raw: string) => ParsedUrl | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Whether `url` is `base` or lies under it: the same origin (scheme, host,
|
|
24
|
+
* port) and, when the base has a path, that path as whole segments. A string
|
|
25
|
+
* prefix would also match `https://intake.example.com.evil.test/`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const isUnderBase: ({ url, base, }: {
|
|
28
|
+
url: string;
|
|
29
|
+
base: string;
|
|
30
|
+
}) => boolean;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type ClientEvent } from "./client-sdk-core/index.js";
|
|
2
|
+
import type { RejectionTracker } from "./types.js";
|
|
3
|
+
export interface ErrorCaptureOptions {
|
|
4
|
+
emit(event: ClientEvent): void;
|
|
5
|
+
/**
|
|
6
|
+
* Receives the fatal error's row instead of `emit`, before the previous
|
|
7
|
+
* handler runs: the crash path must stamp that row with the transport held
|
|
8
|
+
* so it cannot be flushed out of the queue before the outbox drains it.
|
|
9
|
+
*/
|
|
10
|
+
onFatal(event: ClientEvent): void;
|
|
11
|
+
unhandledRejections: boolean;
|
|
12
|
+
/** The tracker the host says held the slot before `init()`; runs after ours. */
|
|
13
|
+
previousRejectionTracker?: RejectionTracker;
|
|
14
|
+
}
|
|
15
|
+
export interface ErrorCaptureHandle {
|
|
16
|
+
teardown(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Where `<SazabiRoot>` reports a render or commit error it caught. React 19's
|
|
20
|
+
* renderer hands an error no boundary caught straight to React Native's
|
|
21
|
+
* `ExceptionsManager` (the renderer's `onUncaughtError`), never through
|
|
22
|
+
* `ErrorUtils`, and the release app dies there. The root's boundary records
|
|
23
|
+
* the error through the process-wide sink first (so a root from a second
|
|
24
|
+
* copy of the module reaches the running instance), then rethrows it so
|
|
25
|
+
* React Native's fatal path still runs unchanged.
|
|
26
|
+
*/
|
|
27
|
+
export declare const captureRenderError: (error: unknown) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Uncaught errors route through React Native's global handler; a fatal one
|
|
30
|
+
* terminates the app once the handler returns, which is why the crash outbox
|
|
31
|
+
* runs inside it. The previous handler (React Native's red box in
|
|
32
|
+
* development, a crash reporter's hook) always runs after ours. Render
|
|
33
|
+
* errors reach the same fatal path from `<SazabiRoot>` (`captureRenderError`);
|
|
34
|
+
* a crash reported more than once in one task is recorded once.
|
|
35
|
+
*
|
|
36
|
+
* Hermes exposes one promise-rejection tracker slot and no way to read it:
|
|
37
|
+
* `HermesInternal` is frozen (its methods non-writable and non-configurable,
|
|
38
|
+
* the object non-extensible) and the global that names it is non-writable
|
|
39
|
+
* and non-configurable too (`lib/VM/JSLib/HermesInternal.cpp`,
|
|
40
|
+
* `lib/VM/JSLib/GlobalObject.cpp`), so the installer cannot be wrapped from
|
|
41
|
+
* JavaScript and whoever held the slot before `init()` is unknowable. The
|
|
42
|
+
* host hands that tracker over as `errors.previousRejectionTracker` and ours
|
|
43
|
+
* runs it after recording; `errors.unhandledRejections: false` leaves the
|
|
44
|
+
* slot alone. Without either, a development build is told once per process.
|
|
45
|
+
*/
|
|
46
|
+
export declare const installErrorCapture: (options: ErrorCaptureOptions) => ErrorCaptureHandle;
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type EventAttributes } from "./client-sdk-core/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* react-native-web renders in a real browser, where `@sazabi/browser` owns
|
|
4
|
+
* capture. This SDK installs nothing there.
|
|
5
|
+
*/
|
|
6
|
+
export declare const isWebHost: () => boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Resource attributes the host can report without a native module: OTEL
|
|
9
|
+
* `os.*` from `Platform`, the `device.*` fields Android's
|
|
10
|
+
* `Platform.constants` carries (`Brand`, `Manufacturer`, `Model`), and on iOS
|
|
11
|
+
* the manufacturer plus `device.type` from `interfaceIdiom` (`phone`, `pad`,
|
|
12
|
+
* `tv`, `carplay`, `mac`, `vision`). iOS reports no model through JavaScript.
|
|
13
|
+
* `os.name` stays `Platform.OS` (`ios`, `android`) on both, so queries match
|
|
14
|
+
* one casing; iPadOS shows as `device.type: "pad"`.
|
|
15
|
+
*/
|
|
16
|
+
export declare const hostResourceAttributes: () => EventAttributes;
|
|
17
|
+
export declare const isDevHost: () => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* The Metro (or Expo) development server this bundle was loaded from, as
|
|
20
|
+
* `<origin>/`, read from `SourceCode.scriptURL` the way React Native's own
|
|
21
|
+
* `getDevServer()` does. Only under `__DEV__`, and only for an http(s)
|
|
22
|
+
* bundle URL: a release bundle loads from a file.
|
|
23
|
+
*/
|
|
24
|
+
export declare const devServerOrigin: () => string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* A request URL as the core expects it: absolute, credentials stripped.
|
|
27
|
+
* Relative references are not requests on a native host, so they resolve to
|
|
28
|
+
* nothing and are neither captured nor injected.
|
|
29
|
+
*/
|
|
30
|
+
export declare const resolveUrl: (raw: string) => string | undefined;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type EventSeverity } from "./client-sdk-core/index.js";
|
|
2
|
+
import { SazabiRoot } from "./interactions.js";
|
|
3
|
+
import { trackNavigation } from "./navigation.js";
|
|
4
|
+
import type { ReactNativeSdkConfig } from "./types.js";
|
|
5
|
+
export type { AttributeValue, ClientEvent, ClientEventType, ConsoleCaptureConfig, ConsoleCaptureLevel, ErrorCaptureConfig, RejectionTracker, EventAttributes, EventSeverity, NavigationContainerRefLike, NetworkCaptureConfig, PointerCaptureConfig, ReactNativeSdkConfig, StorageAdapter, } from "./types.js";
|
|
6
|
+
export { SazabiRoot, trackNavigation };
|
|
7
|
+
declare module "react-native" {
|
|
8
|
+
interface ViewProps {
|
|
9
|
+
/** Names this element in touch rows (`app.widget.name`), ahead of `accessibilityLabel` and `testID`. */
|
|
10
|
+
sazabiName?: string;
|
|
11
|
+
/** Keeps the name of this element and everything inside it out of touch rows. */
|
|
12
|
+
sazabiMask?: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface TextProps {
|
|
15
|
+
/** Names this element in touch rows (`app.widget.name`), ahead of `accessibilityLabel` and `testID`. */
|
|
16
|
+
sazabiName?: string;
|
|
17
|
+
/** Keeps the name of this element and everything inside it out of touch rows. */
|
|
18
|
+
sazabiMask?: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface TouchableWithoutFeedbackProps {
|
|
21
|
+
/** Names this element in touch rows (`app.widget.name`), ahead of `accessibilityLabel` and `testID`. */
|
|
22
|
+
sazabiName?: string;
|
|
23
|
+
/** Keeps the name of this element and everything inside it out of touch rows. */
|
|
24
|
+
sazabiMask?: boolean;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export { SDK_VERSION } from "./version.js";
|
|
28
|
+
/**
|
|
29
|
+
* Initialize the SDK. Idempotent: repeat calls are ignored. Installs nothing
|
|
30
|
+
* under react-native-web (use `@sazabi/browser` there). When `consent` is
|
|
31
|
+
* provided, nothing is installed until it resolves true.
|
|
32
|
+
*/
|
|
33
|
+
export declare const init: (config: ReactNativeSdkConfig) => void;
|
|
34
|
+
/** Set the client-asserted user identity (`session.distinct_id`). */
|
|
35
|
+
export declare const identify: (distinctId: string, traits?: Record<string, unknown>) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Logout: clear the client-asserted identity and rotate both session and
|
|
38
|
+
* window ids, so later activity on this device never threads into the
|
|
39
|
+
* previous user's timeline.
|
|
40
|
+
*/
|
|
41
|
+
export declare const reset: () => void;
|
|
42
|
+
/** Emit a custom mark into the session event stream. */
|
|
43
|
+
export declare const addEvent: (name: string, attributes?: Record<string, unknown>) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Emit an application log line (`app.event_type: "log"`) with session context
|
|
46
|
+
* attached. Drops silently before init/consent, like all capture.
|
|
47
|
+
*/
|
|
48
|
+
export declare const log: (severity: EventSeverity, message: string, metadata?: Record<string, unknown>) => void;
|
|
49
|
+
/** Force-flush queued events. */
|
|
50
|
+
export declare const flush: () => Promise<void>;
|
|
51
|
+
/** Tear down all patches/listeners (restoring originals) and flush. */
|
|
52
|
+
export declare const shutdown: () => Promise<void>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{a as K,b as Z,c as ie,d as tt,e as he,f as be,g as nt,h as rt,i as ye,j as ot,k as G,l as it,m as st,n as at,o as dt,p as ct,q as lt,r as ut,s as j,t as ft,u as pt,w as gt,x as vt}from"./chunk-c8j7sr2j.js";var d=globalThis.__sazabiReactNative??={initialized:!1,warnedWebHost:!1,warnedRejectionSlot:!1,generation:0,pendingReset:!1,screen:{}};var ne="@sazabi/react-native",V="0.1.0",H="[@sazabi/react-native]";var ke=()=>globalThis.HermesInternal,Ee=(e)=>{try{e()}catch{}},mt=(e)=>{try{let t=typeof e==="string"?e:typeof e==="object"&&e!==null?JSON.stringify(e)??String(e):String(e);return K(t)}catch{try{return K(Object.prototype.toString.call(e))}catch{return"[unreadable thrown value]"}}},re=(e,t)=>{let n=e instanceof Error?e:void 0,r=(s)=>{try{let l=n?.[s];return typeof l==="string"?K(l):void 0}catch{return}},i=r("name")||(t==="unhandledrejection"?"UnhandledRejection":"Error"),o=n?r("message")??"":mt(e);return{type:"error",body:K(`${i}: ${o}`,512),severity:"ERROR",timeUnixMs:Date.now(),attributes:{"exception.type":i,"exception.message":o,"exception.stacktrace":r("stack")??"","app.error.kind":t}}},we=(e)=>{try{d.renderErrorSink?.(e)}catch{}},ht=(e)=>{if(e instanceof Error)return e.stack||`${e.name}: ${e.message}`;if(typeof e==="string")return e;try{return JSON.stringify(e)??String(e)}catch{return String(e)}},oe=(e)=>{if(!j())return;try{let t=e();ie(()=>console.warn(t))}catch{}},Re=(e)=>{let t=globalThis.ErrorUtils,n=t?.getGlobalHandler?.(),r=!0,i=!1,o=(l)=>{if(!r||i)return;i=!0,setTimeout(()=>{i=!1},0),e.onFatal(re(l,"fatal"))},s=(l,u)=>{let c=()=>n?.(l,u);if(!r){c();return}try{if(u)o(l);else e.emit(re(l,"uncaught"))}catch{}ie(c)};if(t?.setGlobalHandler?.(s),d.renderErrorSink=o,e.unhandledRejections){let l=e.previousRejectionTracker;if(!l&&!d.warnedRejectionSlot&&j()&&typeof ke()?.enablePromiseRejectionTracker==="function")d.warnedRejectionSlot=!0,oe(()=>`${H} init() takes over Hermes's only promise rejection tracker slot, and a tracker another library installed before it cannot be seen. If one exists, pass errors.previousRejectionTracker to keep it running, or errors.unhandledRejections: false to leave the slot alone.`);let u={allRejections:!0,onUnhandled:(c,f)=>{if(r)try{e.emit(re(f,"unhandledrejection"))}catch{}if(l){Ee(()=>l.onUnhandled(c,f));return}oe(()=>`Possible unhandled promise rejection (id: ${c}):
|
|
2
|
+
${ht(f)}`)},onHandled:(c)=>{if(l){Ee(()=>l.onHandled?.(c));return}oe(()=>`Promise rejection handled (id: ${c})
|
|
3
|
+
This means you can ignore any previous messages of the form "Possible unhandled promise rejection (id: ${c}):"`)}};try{ke()?.enablePromiseRejectionTracker?.(u)}catch{}}return{teardown(){if(r=!1,d.renderErrorSink===o)d.renderErrorSink=void 0;try{if(n&&t?.getGlobalHandler?.()===s)t.setGlobalHandler?.(n)}catch{}}}};import{Component as yt,createElement as bt}from"react";import{View as St}from"react-native";var se=(e)=>{d.tracker=e},kt=60,Et=3,xe=64,wt="sazabiName",Rt="sazabiMask",Te=new Set(["button","link","tab","menuitem","checkbox","radio","switch","togglebutton","imagebutton","searchbox"]),X=(e)=>{if(typeof e!=="string")return;let t=e.replace(/\s+/g," ").trim();if(!t)return;return t.length>xe?`${t.slice(0,xe)}…`:t},xt=(e)=>{let t=e.type??e.elementType;if(typeof t==="function"){let n=t;return X(n.displayName||n.name)}if(typeof t==="object"&&t!==null){let n=t.render??t.type;if(typeof n==="function"){let r=n;return X(r.displayName||r.name)}}return},Tt=(e)=>{try{let t=e;if(!t||typeof t!=="object")return;let n,r=!1,i=!1,o=[];for(let s=0;t&&s<kt;s+=1){let l=t.memoizedProps??void 0;if(l){if(l[Rt])r=!0;if(n===void 0)n=X(l[wt])??X(l.accessibilityLabel)??X(l.testID);if(typeof l.onPress==="function"||typeof l.onPressIn==="function"||typeof l.accessibilityRole==="string"&&Te.has(l.accessibilityRole)||typeof l.role==="string"&&Te.has(l.role))i=!0}if(o.length<Et){let u=xt(t);if(u&&u!=="SazabiRoot")o.unshift(u)}t=t.return}return{id:o.length>0?o.join(" > "):void 0,name:r?void 0:n,tag:o[o.length-1],interactive:i}}catch{return}},Ct=(e)=>{let t=d.tracker;if(!t)return;try{let{changedTouches:n,touches:r}=e.nativeEvent,i=Tt(e._targetInst);for(let o of n)t.down({id:o.identifier,type:"touch",primary:r.length<=1,x:o.pageX,y:o.pageY,pressure:o.force||void 0,target:i})}catch{}},Pt=(e)=>{let t=d.tracker;if(!t)return;try{for(let n of e.nativeEvent.changedTouches)t.move(n.identifier,n.pageX,n.pageY,n.force||void 0)}catch{}},At=(e)=>{let t=d.tracker;if(!t)return;try{for(let n of e.nativeEvent.changedTouches)t.up(n.identifier,n.pageX,n.pageY)}catch{}},_t=(e)=>{let t=d.tracker;if(!t)return;try{for(let n of e.nativeEvent.changedTouches)t.cancel(n.identifier)}catch{}},Nt={flex:1};class Ce extends yt{static displayName="SazabiRoot";state={};static getDerivedStateFromError(e){return we(e),{error:{value:e}}}render(){let e=this.state.error;if(e)throw e.value;return bt(St,{style:Nt,collapsable:!1,onTouchStart:Ct,onTouchMove:Pt,onTouchEnd:At,onTouchCancel:_t},this.props.children)}}import{AppState as Pe}from"react-native";var Ae=(e)=>{let t=Pe.currentState,n=(i)=>{try{if(i===t)return;if(t=i,i!=="active"&&i!=="background"&&i!=="inactive")return;if(e.emit({type:"lifecycle",body:`lifecycle ${i}`,severity:"INFO",timeUnixMs:Date.now(),attributes:{"app.lifecycle.state":i}}),i!=="active")e.onBackground()}catch{}},r;try{r=Pe.addEventListener("change",n)}catch{}return{teardown(){try{r?.remove()}catch{}}}};var ae=()=>d.screen,de=(e,t)=>{d.navigationEmit=e,d.navigationRedactUrl=t},Ot=(e)=>{if(!e)return;let t=Object.keys(e);return t.length>0?t.join(","):void 0},It=(e)=>{let t=()=>{try{let r=e.getCurrentRoute();if(!r)return;let i=d.screen;if(r.key===i.key&&r.name===i.name&&r.path===i.path)return;d.screen={name:r.name,path:r.path,key:r.key},d.navigationEmit?.({type:"navigation",body:`navigation ${r.name}`,severity:"INFO",timeUnixMs:Date.now(),attributes:{"app.navigation.type":"screen","app.screen.name":r.name,"url.path":r.path===void 0?void 0:d.navigationRedactUrl?.(r.path)??r.path,"app.navigation.from":i.name,"app.navigation.params":Ot(r.params)}})}catch{}};t();let n;try{n=e.addListener("state",t)}catch{}return()=>{try{n?.()}catch{}}},_e=()=>{d.screen={},d.navigationEmit=void 0,d.navigationRedactUrl=void 0};var Ht=async(e,t)=>{try{let n=await e.getItem(t);return typeof n==="string"?n:null}catch{return null}},U=(e)=>typeof e?.then==="function",Ne=(e,t)=>{try{let n=e();return U(n)?Promise.resolve(n).then(t,()=>{return}):t(n)}catch{return}},Oe=(e,t)=>Ne(()=>e.getItem(t),(n)=>({value:typeof n==="string"?n:null})),Ie=(e)=>Ne(()=>e.getAllKeys?.(),(t)=>Array.isArray(t)?t.filter((n)=>typeof n==="string"):void 0),q=(e,t,n)=>{try{let r=e.setItem(t,n);return U(r)?Promise.resolve(r).then(()=>!0,()=>!1):!0}catch{return!1}},k=(e,t)=>{try{let n=e.removeItem(t);return U(n)?Promise.resolve(n).then(()=>!0,()=>!1):!0}catch{return!1}},He=(e,t)=>{let n=G(),r=new Set,i=(async()=>{for(let o of t){let s=await Ht(e,o);if(s!==null&&!r.has(o))n.set(o,s)}})();return{store:{get:(o)=>n.get(o),set(o,s){n.set(o,s),r.add(o),q(e,o,s)},remove(o){n.remove(o),r.add(o),k(e,o)}},ready:i}};var le="sazabi.outbox.",De="sazabi.outbox.index",ue="sazabi.outbox.index.unmerged",Be="sazabi.outbox",Fe="sazabi.outbox.unmerged",B="sazabi.outbox.queue",ze="app.outbox.id",Ke=20,je=524288,T=(e)=>`${le}${e}`,Ut=/^(?:[0-9a-f]{16}|legacy|legacy-unmerged)$/,Mt=(e,t)=>(e??[]).filter((n)=>n.startsWith(le)).map((n)=>n.slice(le.length)).filter((n)=>Ut.test(n)&&!t.has(n)),Ue=new WeakMap,P=(e)=>{let t=Ue.get(e);if(t)return t;let n={batches:[],unmerged:[],loaded:!1,inFlight:new Set,discarded:0,unsaved:0};return Ue.set(e,n),n},Xe=(e,t)=>{P(e).report=t},$e=(e,t)=>{let n=P(e);if(n.report===t)n.report=void 0},Ge=(e)=>{if(e.discarded===0&&e.unsaved===0)return;let{discarded:t,unsaved:n}=e;e.discarded=0,e.unsaved=0,e.report?.({diagnostic:"outbox_discarded",attributes:{"app.sdk.outbox_discarded":t,"app.sdk.outbox_unsaved":n}})},fe=(e,t)=>{let n=e.report;if(!n)return;w(t,(r)=>{if(!r&&e.report===n)n({diagnostic:"outbox_persist_failed"})})},w=(e,t)=>U(e)?Promise.resolve(e).then(t):t(e),W=(e)=>e.some(U)?Promise.all(e):e,Me=(e,t)=>W(t.map((n)=>Oe(e,n))),F=(e,t,n)=>n.length===0?k(e,t):q(e,t,JSON.stringify(n)),D=(e)=>e.map((t)=>t.id),Le=(e)=>{let t=new Set;return e.filter((n)=>{if(t.has(n.id))return!1;return t.add(n.id),!0})},ge=(e)=>{let t=P(e);if(!t.loaded)return t.unmergedRead!==!1&&F(e,ue,D(t.unmerged));return w(F(e,De,D(t.batches)),(n)=>n&&t.unmergedRead?w(k(e,ue),()=>n):n)},qe=(e)=>{let t=0,n=0;for(let r=e.length-1;r>=0;r-=1){if(t+=e[r].length,n===Ke||t>je&&n>0)break;n+=1}return e.slice(e.length-n)},pe=(e,t)=>{let n=0,r=0;for(let i=t.length-1;i>=0;i-=1){let o=t[i]?.envelopes??[];n+=o.length;for(let s of o)r+=s.length;if(i<t.length-1&&(n>Ke||r>je)){let s=t.splice(0,i+1);for(let l of s)e.discarded+=l.envelopes?.length??0;return s}}return[]},Q=(e)=>{try{return JSON.parse(e)}catch{return}},Ye=(e)=>Array.isArray(e)?e.filter((t)=>typeof t==="string"):[],J=(e)=>Ye(e?Q(e):void 0),ce=(e,t)=>{let n=t?Q(t):void 0,r=Ye(n).filter((o)=>{let s=Q(o);return typeof s==="object"&&s!==null}),i=!t?0:Array.isArray(n)?n.length-r.length:1;if(e.discarded+=i,i)e.warn?.("discarded a stored crash report that is not valid JSON."),e.warn=void 0;return{envelopes:r,discarded:i}},Lt=(e,t,[n,r,i,o,s],l,u)=>{if(r)t.unmergedRead=!0;else if(t.unmergedRead===void 0)t.unmergedRead=!1;let c=new Set,f=new Map,v=(m)=>m.flatMap((p)=>{let C=f.get(p);if(C)return C;let _=l.get(p),x;if(!_)x=[{id:p}];else if(_.value===null)x=[];else{let{envelopes:O,discarded:I}=ce(t,_.value);if(O.length===0)c.add(p),x=[];else{if(I)F(e,T(p),O);x=[{id:p,envelopes:O}]}}return f.set(p,x),x}),h=v(J(n?.value)),b=v(J(r?.value)),Y=new Set(D([...t.batches,...t.unmerged])),z=v(u.filter((m)=>!Y.has(m)));if(!n&&t.unmergedRead===!1){t.discarded=0,t.unsaved=0;return}let S=new Set(D([...h,...b,...z,...t.batches,...t.unmerged])),N=[],M=(m,p)=>{if(!m||p.length===0||S.has(m))return!0;return S.add(m),c.delete(m),N.push({id:m,envelopes:p}),F(e,T(m),p)},L=[];for(let[m,p,C]of[[i,"legacy",Be],[o,"legacy-unmerged",Fe]]){if(!m?.value)continue;L.push([C,M(p,ce(t,m.value).envelopes)])}let A=s?.value?Q(s.value):void 0;if(typeof A?.discarded==="number"&&A.discarded>0)t.unsaved+=Math.floor(A.discarded);let E=M(typeof A?.id==="string"?A.id:void 0,ce(t,JSON.stringify(A?.envelopes??[])).envelopes),R;if(n)t.batches=Le([...h,...b,...z,...N,...t.unmerged]),t.unmerged=[],t.loaded=!0,R=pe(t,t.batches);else t.unmerged=Le([...b,...N,...t.unmerged]),R=pe(t,t.unmerged);Ge(t),w(ge(e),(m)=>{for(let p of D(R))k(e,T(p));if(!m)return;for(let p of c)k(e,T(p));return w(W([E,...L.map(([,p])=>p)]),([p,...C])=>{for(let[_,[x]]of L.entries())if(C[_])k(e,x);if(!s||!p)return;return w(s.value?k(e,B):!0,()=>{t.queueRead=!0})})})},ve=(e,t)=>{let n=P(e);if(n.loading)return n.loading;n.warn??=t;let r=Ie(e),i=w(Me(e,[De,ue,Be,Fe,B]),(s)=>w(r,(l)=>{let u=[...new Set([...J(s[0]?.value),...J(s[1]?.value)])],c=s[0]&&s[1]?Mt(l,new Set(u)):[],f=[...u,...c];return w(Me(e,f.map(T)),(v)=>{Lt(e,n,s,new Map(f.map((h,b)=>[h,v[b]])),c)})}));if(!U(i)){if(n.loaded)n.loading=Promise.resolve();return Promise.resolve()}let o=i.then(()=>{if(!n.loaded)n.loading=void 0});return n.loading=o,o},Ve=async(e,t,n)=>{let r=P(e),i=n.filter((c)=>!r.inFlight.has(c));if(i.length===0)return;for(let c of i)r.inFlight.add(c);let o;try{o=await Promise.all(i.map((c)=>t.sendEnvelope(c)))}finally{for(let c of i)r.inFlight.delete(c)}let s=new Set(i.filter((c,f)=>o[f]));if(s.size===0)return;let l=[];for(let c of[r.batches,r.unmerged])for(let f of c){let v=f.envelopes?.filter((h)=>!s.has(h));if(!v||v.length===f.envelopes?.length)continue;if(f.envelopes=v,v.length>0)F(e,T(f.id),v);else l.push([c,f])}let u=await W(l.map(([,c])=>w(k(e,T(c.id)),(f)=>f||q(e,T(c.id),"[]"))));l.forEach(([c,f],v)=>{let h=c.indexOf(f);if(u[v]&&h>=0)c.splice(h,1)}),await ge(e)},Qe=(e,t,n)=>{let r=t.drainEnvelopes({[ze]:n}),i=qe(r);if(i.length===0)return!0;let o=P(e);o.discarded+=r.length-i.length;let s=o.loaded?o.batches:o.unmerged;s.push({id:n,envelopes:i});let l=pe(o,s),u=F(e,T(n),i),c=ge(e),f=c===!0&&u===!0;for(let v of D(l))k(e,T(v));if(Ge(o),fe(o,u),o.loaded||o.unmergedRead!==!1)fe(o,c);if(o.queueSaved===t)o.queueSaved=void 0,w(W([u,c]),(v)=>{if(v.every(Boolean)&&o.queueSaved===void 0)k(e,B)});if(!f)Ve(e,t,i);return f},me=async(e,t)=>{await ve(e);let n=P(e);await Ve(e,t,[...new Set([...n.batches,...n.unmerged].flatMap((r)=>r.envelopes??[]))])},Je=(e,t,n)=>{let r=P(e);if(!r.queueRead)return;let i=t.snapshotEnvelopes({[ze]:n}),o=qe(i);if(r.queueSaved=o.length>0?t:void 0,r.queueSaved){let s=i.length-o.length;fe(r,q(e,B,JSON.stringify(s>0?{id:n,envelopes:o,discarded:s}:{id:n,envelopes:o})))}else k(e,B)},We=(e,t)=>{let n=P(e);if(n.queueSaved!==t)return;n.queueSaved=void 0,k(e,B)};var Dt=5000,Bt=57344,Ft=500,zt=200,Kt=["x-request-id","cf-ray","x-vercel-id","x-amz-cf-id","fly-request-id"],jt=["sazabi.session","sazabi.distinctId"],Xt=2000,$t="javascript",Ze=(e,t)=>({type:"custom",body:`identify ${e}`,severity:"INFO",timeUnixMs:Date.now(),attributes:{"app.custom.name":"identify",...t?Z(t,"custom"):{}}}),Gt=(e)=>{let t=globalThis.fetch;if(typeof t!=="function")return;let n=(a,g)=>t(a,g),r=be(e);if(!r)return;let i=`${r.baseUrl}/v1/logs`,o=rt(),s=e.storage,l=(a)=>console.warn(`${H} ${a}`),u=ct({url:i,publicKey:r.publicKey,resourceAttributes:{"service.name":e.serviceName,"service.version":e.serviceVersion,"deployment.environment":e.environment,"telemetry.sdk.name":ne,"telemetry.sdk.version":V,"telemetry.sdk.language":$t,...ut()},scope:{name:ne,version:V},fetchImpl:n,flushIntervalMs:e.flushIntervalMs??Dt,maxBatchBytes:e.maxBatchBytes??Bt,maxQueuedEvents:e.maxQueuedEvents??Ft,warn:l,onHealth:(a)=>M(a),onIdle:()=>{if(s)We(s,u)}}),c,f=[],v=0,h=!1,b=e.network??{},Y=(a)=>nt(a,b.redactQueryParams),z=(a,g,y=ae())=>{g.touch();let te={"app.event_type":a.type,"session.id":g.getSessionId(),"session.window_id":g.getWindowId(),"session.distinct_id":g.getDistinctId(),"app.screen.name":y.name,"app.screen.url":y.path===void 0?void 0:Y(y.path),"derived.source":"sdk"};u.enqueue(dt(a,te))},S=(a)=>{if(h)return;try{if(c)z(a,c);else if(v<zt||a.type==="sdk_health")f.push({kind:"event",event:a,screen:ae()}),v+=1}catch{}},N=(a)=>{try{S(a())}catch{}},M=at(S);if(s)Xe(s,M),ve(s,l);let L=ft(),A=(a)=>he({url:a,base:r.baseUrl})||L!==void 0&&he({url:a,base:L})||j()&&a.includes("/symbolicate"),E=[];if(s)E.push(()=>$e(s,M));let R,m=!1;E.push(()=>{if(R!==void 0)clearTimeout(R);R=void 0});let p=Re({emit:S,unhandledRejections:e.errors?.unhandledRejections!==!1,previousRejectionTracker:e.errors?.previousRejectionTracker,onFatal:(a)=>{m=!0,u.holdFlushes(!0);try{if(ee(),S(a),s)Qe(s,u,ye(o,8))}finally{u.holdFlushes(!1)}if(!s){u.flush();return}if(R===void 0&&!h)R=setTimeout(()=>{R=void 0,me(s,u)},Xt)}});if(E.push(()=>p.teardown()),e.pointer?.capture!==!1){let a=ot({emit:S,randomBytes:o,sampleIntervalMs:e.pointer?.sampleIntervalMs,partialIntervalMs:e.pointer?.partialIntervalMs});se(a),E.push(()=>{se(void 0),a.teardown()})}if(de(S,Y),E.push(()=>de(void 0)),e.console?.capture!==!1){let a=tt({emit:S,levels:e.console?.levels??["error","warn"],selfPrefix:H});E.push(()=>a.teardown())}let C=b.capture??!0,_=b.propagateTraceContext??!0;if(C||_){let a=vt({emit:S,traceContext:st({sampledFlag:b.sampledFlag??!1,randomBytes:o}),capture:C,propagateTraceContext:_,allowlist:b.allowlist??[],redactQueryParams:b.redactQueryParams,requestIdHeaders:b.requestIdHeaders??Kt,isInternalUrl:A,resolveUrl:pt,isDefaultInjectionTarget:()=>!0});E.push(()=>a.teardown())}let x=Ae({emit:S,onBackground:()=>{if(s)Je(s,u,ye(o,8));u.flush()}});E.push(()=>x.teardown());let O,I;if(s){let a=He(s,jt);O=a.store,I=a.ready}else O=G(),I=Promise.resolve();let ee=()=>{if(c||h)return;let a=it({store:O,windowStore:G(),randomBytes:o});if(c=a,d.pendingReset)a.reset(),d.pendingReset=!1;let g=d.pendingIdentity;if(d.pendingIdentity=void 0,g)a.setDistinctId(g.distinctId),S(Ze(g.distinctId,g.traits));for(let y of f.splice(0))try{if(y.kind==="identify")a.setDistinctId(y.distinctId);else if(y.kind==="reset")a.reset();else z(y.event,a,y.screen)}catch{}if(s&&!m)me(s,u)},Se=(a)=>{let g=f.length;while(g>0&&f[g-1]?.kind!=="event")g-=1;let y=f.splice(g);if(a.kind==="reset"||y.some((te)=>te.kind==="reset"))f.push({kind:"reset"});if(a.kind==="identify")f.push(a)};d.instance={identify(a,g){N(()=>{if(c)c.setDistinctId(a);else Se({kind:"identify",distinctId:a});return Ze(a,g)})},reset(){try{if(c)c.reset();else Se({kind:"reset"})}catch{}},addEvent(a,g){N(()=>({type:"custom",body:`custom ${a}`,severity:"INFO",timeUnixMs:Date.now(),attributes:{"app.custom.name":a,...g?Z(g,"custom"):{}}}))},log(a,g,y){N(()=>({type:"log",body:g,severity:a,timeUnixMs:Date.now(),attributes:y?Z(y):{}}))},async flush(){await I,await u.flush()},async shutdown(){for(let a of E.reverse())try{a()}catch{}h=!0,gt(),_e(),await I,await u.shutdown()}},I.then(ee,ee)},et=(e)=>{try{Gt(e)}catch{}},xn=(e)=>{if(lt()){if(!d.warnedWebHost)d.warnedWebHost=!0,console.warn(`${H} this app is running in a browser; use @sazabi/browser instead. SDK disabled.`);return}if(d.initialized)return;d.initialized=!0,d.generation+=1;let t=d.generation;if(!e.serviceName||!be(e)){console.warn(`${H} init() requires intakeUrl and serviceName; SDK disabled. Pass publicKey too if the intake URL is a proxy that carries no key.`);return}if(e.consent===void 0){et(e);return}try{Promise.resolve(e.consent()).then((n)=>{if(n===!0&&t===d.generation)et(e)}).catch(()=>{})}catch{}},Tn=(e,t)=>{if(d.instance)d.instance.identify(e,t);else d.pendingIdentity={distinctId:e,traits:t}},Cn=()=>{if(d.instance)d.instance.reset();else d.pendingReset=!0,d.pendingIdentity=void 0},Pn=(e,t)=>{d.instance?.addEvent(e,t)},An=(e,t,n)=>{d.instance?.log(e,t,n)},_n=async()=>{await d.instance?.flush()},Nn=async()=>{let e=d.instance;d.instance=void 0,d.initialized=!1,d.generation+=1,d.pendingIdentity=void 0,d.pendingReset=!1,await e?.shutdown()};export{V as SDK_VERSION,Ce as SazabiRoot,Pn as addEvent,_n as flush,Tn as identify,xn as init,An as log,Cn as reset,Nn as shutdown,It as trackNavigation};
|
|
4
|
+
|
|
5
|
+
//# debugId=ACB6EA50D2850DE964756E2164756E21
|
|
6
|
+
//# sourceMappingURL=index.js.map
|