@refraction-ui/react 0.5.0 → 0.7.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.
@@ -0,0 +1,448 @@
1
+ /**
2
+ * @refraction-ui/analytics — type contracts.
3
+ *
4
+ * The library is a neutral Segment-spec collector/router. The app instruments
5
+ * once via the canonical API; the router fans the canonical envelope out to
6
+ * N pluggable sinks. There is NO privileged engine — every vendor is a sink.
7
+ */
8
+ /** Canonical Segment event types. */
9
+ type AnalyticsEventType = 'track' | 'identify' | 'page' | 'screen' | 'group' | 'alias';
10
+ /** Schema version stamped onto every envelope and the wire contract path. */
11
+ declare const SCHEMA_VERSION = 1;
12
+ /** Arbitrary, JSON-serialisable bag of values. */
13
+ type AnalyticsProperties = Record<string, unknown>;
14
+ /**
15
+ * The context block attached to every event. `library` identifies the
16
+ * collector; `app`/`env` identify the product instance.
17
+ */
18
+ interface AnalyticsContext {
19
+ app: string;
20
+ env: string;
21
+ page?: {
22
+ path?: string;
23
+ url?: string;
24
+ referrer?: string;
25
+ title?: string;
26
+ search?: string;
27
+ };
28
+ library: {
29
+ name: string;
30
+ version: string;
31
+ };
32
+ /** Free-form additions contributed by `with(context)` children. */
33
+ [key: string]: unknown;
34
+ }
35
+ /**
36
+ * The canonical Segment envelope. Every sink receives events in exactly this
37
+ * shape; the built-in HTTP sink ships it verbatim over the wire contract.
38
+ */
39
+ interface AnalyticsEvent {
40
+ /** Segment call type. */
41
+ type: AnalyticsEventType;
42
+ /** Event name (track/screen) or page name (page). */
43
+ event?: string;
44
+ /** Idempotency key — backends MUST dedupe on this. */
45
+ messageId: string;
46
+ /** Persistent, non-PII, resettable client id. */
47
+ anonymousId: string;
48
+ /** Opaque app-supplied id (set after identify). */
49
+ userId?: string;
50
+ /** Group id (group calls). */
51
+ groupId?: string;
52
+ /** Previous id (alias calls). */
53
+ previousId?: string;
54
+ /** Analytics session id (UUIDv4, minted at session start). */
55
+ sessionId: string;
56
+ /** track/page/screen/group payload. */
57
+ properties?: AnalyticsProperties;
58
+ /** identify/group trait payload. */
59
+ traits?: AnalyticsProperties;
60
+ /** Collector + product context. */
61
+ context: AnalyticsContext;
62
+ /** ISO-8601 client timestamp. */
63
+ timestamp: string;
64
+ /** Envelope schema version. */
65
+ schemaVersion: number;
66
+ }
67
+ /**
68
+ * Sink SPI — implemented by adapter packages (GA4, Azure, PostHog) and by the
69
+ * built-in HTTP sink. A sink declares the consent categories it requires; the
70
+ * router will not deliver to a sink whose categories are not all granted.
71
+ */
72
+ interface AnalyticsSink {
73
+ /** Stable sink identifier. */
74
+ name: string;
75
+ /** Consent categories this sink requires (e.g. ['analytics']). */
76
+ consentCategories?: string[];
77
+ /** Called once before the first delivery. */
78
+ init?(ctx: SinkInitContext): void | Promise<void>;
79
+ /** Deliver a batch of canonical events. */
80
+ deliver(batch: AnalyticsEvent[], ctx: SinkDeliverContext): void | Promise<void>;
81
+ /** Flush any buffered state (best-effort). */
82
+ flush?(): void | Promise<void>;
83
+ /** Release resources on reset()/shutdown. */
84
+ shutdown?(): void | Promise<void>;
85
+ }
86
+ /** Context handed to `sink.init`. */
87
+ interface SinkInitContext {
88
+ app: string;
89
+ env: string;
90
+ endpoint?: string;
91
+ }
92
+ /** Context handed to every `sink.deliver`. */
93
+ interface SinkDeliverContext {
94
+ /** True on the unload path — sinks should prefer sendBeacon. */
95
+ unload: boolean;
96
+ }
97
+ /** Cross-tab persistence SPI. Defaults pick localStorage/cookie/memory. */
98
+ interface AnalyticsStorage {
99
+ get(key: string): string | null;
100
+ set(key: string, value: string): void;
101
+ remove(key: string): void;
102
+ }
103
+ /** Session engine configuration. */
104
+ interface SessionConfig {
105
+ /** Inactivity timeout in ms before a new session is minted. GA4 = 30min. */
106
+ timeoutMs?: number;
107
+ /** Reset the session when a campaign/utm change is detected. */
108
+ resetOnCampaign?: boolean;
109
+ /** Persistence backend; defaults to localStorage→cookie→memory. */
110
+ storage?: AnalyticsStorage;
111
+ /** Storage key namespace. */
112
+ storageKey?: string;
113
+ }
114
+ /** Identity engine configuration. */
115
+ interface IdentityConfig {
116
+ /** Persistence backend; defaults to localStorage→cookie→memory. */
117
+ storage?: AnalyticsStorage;
118
+ /** Storage key namespace. */
119
+ storageKey?: string;
120
+ }
121
+ /** Consent gate configuration. */
122
+ interface ConsentConfig {
123
+ /** Categories granted at boot. */
124
+ granted?: string[];
125
+ /**
126
+ * If true, an event is dropped entirely when NO sink can receive it.
127
+ * If false (default), events are still buffered until consent is granted.
128
+ */
129
+ strict?: boolean;
130
+ }
131
+ /** Consent gate runtime API. */
132
+ interface ConsentAPI {
133
+ grant(...categories: string[]): void;
134
+ revoke(...categories: string[]): void;
135
+ granted(): string[];
136
+ isGranted(category: string): boolean;
137
+ }
138
+ /** Session runtime API. */
139
+ interface SessionAPI {
140
+ /** Current session id (mints one lazily if none active). */
141
+ id(): string;
142
+ /** Force-start a new session, returning the new id. */
143
+ start(): string;
144
+ /** End the current session. */
145
+ end(): void;
146
+ /** Attach arbitrary session-scoped properties. */
147
+ set(props: AnalyticsProperties): void;
148
+ }
149
+ /** Built-in HTTP sink options (Segment HTTP Tracking API wire contract). */
150
+ interface HttpSinkOptions {
151
+ /** Base endpoint; events POST to `{endpoint}/v{schemaVersion}/batch`. */
152
+ endpoint: string;
153
+ /** Write key — sent as `Authorization: Basic base64(writeKey:)`. */
154
+ writeKey: string;
155
+ /** Max retries for 429/5xx (exponential backoff). Default 3. */
156
+ maxRetries?: number;
157
+ /** Base backoff delay in ms. Default 500. */
158
+ backoffBaseMs?: number;
159
+ /** Injected fetch (defaults to global fetch). */
160
+ fetchImpl?: typeof fetch;
161
+ /** Injected sendBeacon (defaults to navigator.sendBeacon). */
162
+ beaconImpl?: (url: string, body: string) => boolean;
163
+ /** Consent categories this sink requires. Default ['analytics']. */
164
+ consentCategories?: string[];
165
+ /** Soft per-batch byte cap (Segment ≈ 500KB). Default 500_000. */
166
+ maxBatchBytes?: number;
167
+ /** Soft per-event byte cap (Segment ≈ 32KB). Default 32_000. */
168
+ maxEventBytes?: number;
169
+ }
170
+ /** `createAnalytics` configuration. */
171
+ interface AnalyticsConfig {
172
+ /** Product/app identifier (stamped into context.app). */
173
+ app: string;
174
+ /** Environment, e.g. 'development' | 'production'. Drives presets. */
175
+ env: string;
176
+ /** When set, a built-in HTTP sink is auto-registered to this endpoint. */
177
+ endpoint?: string;
178
+ /** Write key for the auto-registered HTTP sink. */
179
+ writeKey?: string;
180
+ /** Kill switch — when false, a tree-shakeable noop is returned. Default true. */
181
+ enabled?: boolean;
182
+ /** 0..1 sampling rate applied per top-level call. Default 1. */
183
+ sampleRate?: number;
184
+ /** Extra property/trait keys to redact (in addition to the PII deny-list). */
185
+ redactKeys?: string[];
186
+ /** Explicit sinks (in addition to / instead of the auto HTTP sink). */
187
+ sinks?: AnalyticsSink[];
188
+ /** Session engine config. */
189
+ session?: SessionConfig;
190
+ /** Identity engine config. */
191
+ identity?: IdentityConfig;
192
+ /** Consent gate config. */
193
+ consent?: ConsentConfig;
194
+ /**
195
+ * Force a preset. Defaults: env==='production' → 'prod', else 'dev'.
196
+ * dev = sync + console; prod = batch + sample + beacon flush on unload.
197
+ */
198
+ preset?: 'dev' | 'prod';
199
+ /** Batch size before an automatic flush (prod). Default 20. */
200
+ batchSize?: number;
201
+ /** Auto-flush interval in ms (prod). Default 10_000. */
202
+ flushIntervalMs?: number;
203
+ }
204
+ /** Options accepted by every top-level call. */
205
+ interface CallOptions {
206
+ /** Per-call timestamp override (ISO-8601). */
207
+ timestamp?: string;
208
+ /** Per-call context overrides (shallow-merged). */
209
+ context?: Partial<AnalyticsContext>;
210
+ }
211
+ /** The public analytics surface returned by `createAnalytics`. */
212
+ interface Analytics {
213
+ track(event: string, properties?: AnalyticsProperties, opts?: CallOptions): void;
214
+ identify(userId: string, traits?: AnalyticsProperties, opts?: CallOptions): void;
215
+ page(name?: string, properties?: AnalyticsProperties, opts?: CallOptions): void;
216
+ screen(name?: string, properties?: AnalyticsProperties, opts?: CallOptions): void;
217
+ group(groupId: string, traits?: AnalyticsProperties, opts?: CallOptions): void;
218
+ alias(userId: string, previousId?: string, opts?: CallOptions): void;
219
+ /** Analytics-session API (NOT replay). */
220
+ session: SessionAPI;
221
+ /** Consent gate API. */
222
+ consent: ConsentAPI;
223
+ /** Persistent, non-PII anonymous id. */
224
+ anonymousId(): string;
225
+ /** Current opaque user id (if identified). */
226
+ userId(): string | undefined;
227
+ /** Derive a child that merges extra context into every event. */
228
+ with(context: Partial<AnalyticsContext>): Analytics;
229
+ /** Register an additional sink at runtime. */
230
+ addSink(sink: AnalyticsSink): void;
231
+ /** Remove a sink by name. */
232
+ removeSink(name: string): void;
233
+ /** Registered sink names. */
234
+ readonly sinks: string[];
235
+ /** Flush all sinks (and the internal batch). */
236
+ flush(): Promise<void>;
237
+ /**
238
+ * Reset identity + session (privacy-safe logout). Mints a fresh
239
+ * anonymousId and ends the session.
240
+ */
241
+ reset(): void;
242
+ /** Whether this is a live collector (false for the noop). */
243
+ readonly enabled: boolean;
244
+ }
245
+
246
+ /**
247
+ * createAnalytics — the neutral Segment-spec collector/router.
248
+ *
249
+ * Mirrors `createAI`: a single factory returns the entire public surface and
250
+ * fans the canonical envelope out to registered sinks. There is NO privileged
251
+ * engine — the built-in HTTP sink and every vendor adapter are equal sinks.
252
+ *
253
+ * Presets:
254
+ * dev — synchronous delivery + a console sink (see exactly what ships).
255
+ * prod — batching + sampling + beacon flush on pagehide/visibilitychange.
256
+ *
257
+ * When `enabled: false`, a tree-shakeable noop is returned and none of the
258
+ * live collector / sink code is reachable.
259
+ */
260
+ declare function createAnalytics(config: AnalyticsConfig): Analytics;
261
+
262
+ /** Create the built-in Segment-spec HTTP sink. */
263
+ declare function createHttpSink(options: HttpSinkOptions): AnalyticsSink;
264
+
265
+ interface ConsoleSinkOptions {
266
+ /** Injected logger (defaults to globalThis.console). */
267
+ logger?: Pick<Console, 'log' | 'groupCollapsed' | 'groupEnd'>;
268
+ /** Consent categories this sink requires. Default: none (always allowed). */
269
+ consentCategories?: string[];
270
+ }
271
+ /**
272
+ * Built-in `console` sink — the dev preset's default. Prints each canonical
273
+ * envelope so engineers can see exactly what would ship over the wire.
274
+ */
275
+ declare function createConsoleSink(options?: ConsoleSinkOptions): AnalyticsSink;
276
+
277
+ /** A mock sink that records every call — used for testing the router. */
278
+ interface MockSink extends AnalyticsSink {
279
+ /** All events received across all deliver() calls (flattened). */
280
+ events: AnalyticsEvent[];
281
+ /** Each deliver() call's batch + context. */
282
+ deliveries: Array<{
283
+ batch: AnalyticsEvent[];
284
+ ctx: SinkDeliverContext;
285
+ }>;
286
+ /** init() call contexts. */
287
+ initCalls: SinkInitContext[];
288
+ /** flush() invocation count. */
289
+ flushCalls: number;
290
+ /** shutdown() invocation count. */
291
+ shutdownCalls: number;
292
+ }
293
+ interface CreateMockSinkOptions {
294
+ name?: string;
295
+ consentCategories?: string[];
296
+ }
297
+ /**
298
+ * createMockSink — an `AnalyticsSink` that captures everything for assertion.
299
+ * Mirrors the testing ergonomics of `@refraction-ui/ai`'s mock providers.
300
+ */
301
+ declare function createMockSink(options?: CreateMockSinkOptions): MockSink;
302
+
303
+ /** GA4 parity: 30 minutes of inactivity ends a session. */
304
+ declare const DEFAULT_SESSION_TIMEOUT_MS: number;
305
+ /**
306
+ * Derive a stable campaign fingerprint from a URL's query string. A change in
307
+ * this fingerprint (a *new* campaign, not its absence) forces a new session,
308
+ * matching GA4's "campaign change resets the session" behaviour.
309
+ */
310
+ declare function campaignFingerprint(search?: string): string | undefined;
311
+ /**
312
+ * Session engine.
313
+ *
314
+ * A session is a span of continuous activity. It ends after `timeoutMs` of
315
+ * inactivity (GA4 parity, default 30 min) or when a *new* campaign is
316
+ * detected. State is persisted cross-tab so multiple tabs share one session.
317
+ */
318
+ declare function createSession(config?: SessionConfig, now?: () => number): {
319
+ /** Get the current session id, rotating if expired. */
320
+ id(campaign?: string): string;
321
+ /** Force a brand-new session. */
322
+ start(campaign?: string): string;
323
+ /** End the current session (next id() mints a fresh one). */
324
+ end(): void;
325
+ /** Touch activity so the inactivity window slides forward. */
326
+ touch(campaign?: string): string;
327
+ /** Attach/merge session-scoped properties. */
328
+ set(props: AnalyticsProperties): void;
329
+ /** Read session-scoped properties (undefined when none). */
330
+ props(): AnalyticsProperties | undefined;
331
+ };
332
+
333
+ /**
334
+ * Identity engine.
335
+ *
336
+ * - `anonymousId`: persistent, non-PII, resettable UUIDv4 stored cross-tab.
337
+ * - `userId`: opaque, app-supplied; never persisted by the library (the app
338
+ * owns the user record) — kept only in memory for the active page.
339
+ * - `alias`: records a previous→current stitch for the wire envelope.
340
+ */
341
+ declare function createIdentity(config?: IdentityConfig): {
342
+ anonymousId(): string;
343
+ userId(): string | undefined;
344
+ /** identify(): bind an opaque app user id (no validation, no persistence). */
345
+ setUserId(id: string): void;
346
+ /**
347
+ * alias(): returns the stitch pair for the envelope. `previousId`
348
+ * defaults to the current user or anonymous id.
349
+ */
350
+ alias(nextUserId: string, previousId?: string): {
351
+ userId: string;
352
+ previousId: string;
353
+ };
354
+ /**
355
+ * reset(): privacy-safe logout. Drops the user binding and mints a brand
356
+ * new anonymousId so the next visitor is not stitched to the old one.
357
+ */
358
+ reset(): string;
359
+ };
360
+
361
+ /**
362
+ * Consent gate.
363
+ *
364
+ * Holds the set of granted consent categories. The router asks the gate, per
365
+ * sink, whether *all* of that sink's required categories are granted before
366
+ * delivering. A sink with no declared categories is always allowed (it is the
367
+ * sink author's responsibility to declare what it needs).
368
+ */
369
+ declare function createConsent(config?: ConsentConfig): ConsentAPI & {
370
+ /** True when every required category is granted (empty list ⇒ allowed). */
371
+ allows(required?: string[]): boolean;
372
+ /** True when at least one sink could receive (used by strict mode). */
373
+ strict: boolean;
374
+ };
375
+
376
+ /**
377
+ * PII redaction.
378
+ *
379
+ * A built-in deny-list of well-known PII key names (email/phone/name and
380
+ * common variants) plus any caller-supplied `redactKeys`. Matching is
381
+ * case-insensitive and substring-based so `userEmail`, `email_address`,
382
+ * `phoneNumber`, `fullName`, etc. are all caught. Redaction recurses into
383
+ * nested objects and arrays so PII cannot hide one level down.
384
+ */
385
+ /**
386
+ * Built-in PII deny-list (substring, case-insensitive, separator-insensitive).
387
+ * These match anywhere in a key, so `userEmail`, `email_address`,
388
+ * `phoneNumber`, `firstName`, etc. are all caught.
389
+ */
390
+ declare const PII_DENY_LIST: readonly string[];
391
+ /**
392
+ * Keys that are PII only as an *exact* (normalised) match. `name` belongs
393
+ * here so genuine name fields redact while `username`, `eventName`,
394
+ * `fileName`, `firstName`-style compounds (handled by the deny-list) do not
395
+ * over-redact every key that merely contains "name".
396
+ */
397
+ declare const PII_EXACT_KEYS: readonly string[];
398
+ /** Replacement token written in place of a redacted value. */
399
+ declare const REDACTED = "[REDACTED]";
400
+ /**
401
+ * Build a key matcher from the deny-list + extra keys. `extra` entries match
402
+ * exactly (case-insensitive, normalised); deny-list entries match as
403
+ * substrings.
404
+ */
405
+ declare function createRedactor(extraKeys?: string[]): {
406
+ shouldRedact: (key: string) => boolean;
407
+ /** Redact a properties/traits bag (returns a new object). */
408
+ redact(props?: AnalyticsProperties): AnalyticsProperties | undefined;
409
+ };
410
+ type Redactor = ReturnType<typeof createRedactor>;
411
+
412
+ /**
413
+ * Storage adapters.
414
+ *
415
+ * Order of preference for the cross-tab default: localStorage → cookie →
416
+ * in-memory. The package is environment-agnostic; consumers can inject any
417
+ * `AnalyticsStorage` (e.g. an RN AsyncStorage shim) via config.
418
+ */
419
+ /** Volatile per-process store (SSR / Node / no-DOM fallback). */
420
+ declare function createMemoryStorage(): AnalyticsStorage;
421
+ /** `window.localStorage` adapter (cross-tab via the storage event). */
422
+ declare function createLocalStorageAdapter(ls: Storage): AnalyticsStorage;
423
+ interface CookieDoc {
424
+ cookie: string;
425
+ }
426
+ /** `document.cookie` adapter (cross-tab + cross-subdomain capable). */
427
+ declare function createCookieAdapter(doc: CookieDoc, maxAgeSeconds?: number): AnalyticsStorage;
428
+ /**
429
+ * Resolve the default storage for the current environment. A caller-supplied
430
+ * storage always wins. Browser → localStorage (falls back to cookie if
431
+ * localStorage throws), otherwise in-memory.
432
+ */
433
+ declare function resolveStorage(override?: AnalyticsStorage): AnalyticsStorage;
434
+
435
+ /**
436
+ * uuidv4 — RFC 4122 version 4 UUID.
437
+ *
438
+ * Prefers `crypto.randomUUID` / `crypto.getRandomValues` when available
439
+ * (browser, modern Node) and degrades to `Math.random` only as a last
440
+ * resort. No external dependency by design (this is the neutral router).
441
+ */
442
+ declare function uuidv4(): string;
443
+ /** RFC 4122 v4 shape matcher (case-insensitive). */
444
+ declare const UUID_V4_RE: RegExp;
445
+ /** True when `value` is a well-formed v4 UUID. */
446
+ declare function isUuidV4(value: unknown): value is string;
447
+
448
+ export { type Analytics, type AnalyticsConfig, type AnalyticsContext, type AnalyticsEvent, type AnalyticsEventType, type AnalyticsProperties, type AnalyticsSink, type AnalyticsStorage, type CallOptions, type ConsentAPI, type ConsentConfig, type ConsoleSinkOptions, type CreateMockSinkOptions, DEFAULT_SESSION_TIMEOUT_MS, type HttpSinkOptions, type IdentityConfig, type MockSink, PII_DENY_LIST, PII_EXACT_KEYS, REDACTED, type Redactor, SCHEMA_VERSION, type SessionAPI, type SessionConfig, type SinkDeliverContext, type SinkInitContext, UUID_V4_RE, campaignFingerprint, createAnalytics, createConsent, createConsoleSink, createCookieAdapter, createHttpSink, createIdentity, createLocalStorageAdapter, createMemoryStorage, createMockSink, createRedactor, createSession, isUuidV4, resolveStorage, uuidv4 };
@@ -0,0 +1,229 @@
1
+ /** Severity levels, ordered low -> high. */
2
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
3
+ /** Numeric ordering used for level threshold comparisons. */
4
+ declare const LEVEL_ORDER: Record<LogLevel, number>;
5
+ /** Bound key/value pairs attached to every record emitted by a logger. */
6
+ type LogContext = Record<string, unknown>;
7
+ /** A single structured log record handed to a sink. */
8
+ interface LogRecord {
9
+ level: LogLevel;
10
+ message: string;
11
+ timestamp: number;
12
+ app: string;
13
+ env: TelemetryEnv;
14
+ /** Merged bound context (child loggers) + per-call context, post-redaction. */
15
+ context: LogContext;
16
+ }
17
+ /** A span record handed to a sink when a span ends. */
18
+ interface SpanRecord {
19
+ name: string;
20
+ startTime: number;
21
+ endTime: number;
22
+ durationMs: number;
23
+ app: string;
24
+ env: TelemetryEnv;
25
+ /** Merged bound context + span attributes, post-redaction. */
26
+ context: LogContext;
27
+ /** 'ok' unless ended with an error. */
28
+ status: 'ok' | 'error';
29
+ error?: {
30
+ name: string;
31
+ message: string;
32
+ };
33
+ }
34
+ /**
35
+ * Vendor-neutral telemetry sink. Engines (console, Faro, custom) implement
36
+ * this — no vendor type ever leaks across this boundary.
37
+ */
38
+ interface TelemetrySink {
39
+ /** Engine name, for diagnostics. */
40
+ name: string;
41
+ /** Receive a log record. May buffer; honor flush(). */
42
+ log(record: LogRecord): void;
43
+ /** Receive a finished span. May buffer; honor flush(). */
44
+ span(record: SpanRecord): void;
45
+ /** Force-deliver any buffered records. Resolves once delivery is attempted. */
46
+ flush(): Promise<void>;
47
+ }
48
+ /** Telemetry environment — selects a behavior preset. */
49
+ type TelemetryEnv = 'development' | 'production';
50
+ /** Config for {@link createTelemetry} — mirrors `createAI`'s config shape. */
51
+ interface TelemetryConfig {
52
+ /** Logical app/service name attached to every record. */
53
+ app: string;
54
+ /** Environment preset selector. */
55
+ env: TelemetryEnv;
56
+ /**
57
+ * Remote collector endpoint. When omitted, telemetry stays console-only
58
+ * (no network engine is constructed).
59
+ */
60
+ endpoint?: string;
61
+ /**
62
+ * Master kill switch. When `false`, a tree-shakeable noop logger is
63
+ * returned and zero records are ever produced. Defaults to `true`.
64
+ */
65
+ enabled?: boolean;
66
+ /**
67
+ * Fraction of records kept, 0..1. Defaults to the preset value
68
+ * (1 in development, 0.25 in production). Records below the sample
69
+ * are dropped before reaching any sink.
70
+ */
71
+ sampleRate?: number;
72
+ /**
73
+ * Context keys to strip (deep) before a record is emitted. Use for
74
+ * PII / secrets, e.g. `['password', 'token', 'authorization']`.
75
+ */
76
+ redactKeys?: string[];
77
+ }
78
+ /** A logger bound to a context. Child loggers inherit + extend that context. */
79
+ interface Logger {
80
+ debug(message: string, context?: LogContext): void;
81
+ info(message: string, context?: LogContext): void;
82
+ warn(message: string, context?: LogContext): void;
83
+ error(message: string, context?: LogContext): void;
84
+ fatal(message: string, context?: LogContext): void;
85
+ /**
86
+ * Derive a logger with additional bound context (e.g.
87
+ * `{ sessionId, interviewId, turnId }`). Merges over the parent's context.
88
+ */
89
+ child(context: LogContext): Logger;
90
+ /** Begin a span. Call {@link Span.end} to record its duration. */
91
+ startSpan(name: string, attributes?: LogContext): Span;
92
+ /** Force-deliver buffered records on every sink. */
93
+ flush(): Promise<void>;
94
+ }
95
+ /** An in-flight span returned by {@link Logger.startSpan}. */
96
+ interface Span {
97
+ /** End the span and emit a {@link SpanRecord}. Optionally attach error/attrs. */
98
+ end(opts?: {
99
+ error?: unknown;
100
+ attributes?: LogContext;
101
+ }): void;
102
+ }
103
+ /** Return type of {@link createTelemetry}. */
104
+ interface Telemetry extends Logger {
105
+ /** Registered sink names, in insertion order. */
106
+ readonly sinks: string[];
107
+ /** Register an additional sink (e.g. a custom collector). */
108
+ addSink(sink: TelemetrySink): void;
109
+ /** Remove a sink by name. */
110
+ removeSink(name: string): void;
111
+ }
112
+
113
+ /**
114
+ * Behavior derived from {@link TelemetryConfig.env}. The manager reads these
115
+ * fields to decide level filtering, batching, sampling, and flush triggers.
116
+ */
117
+ interface TelemetryPreset {
118
+ /** Records strictly below this level are dropped. */
119
+ minLevel: LogLevel;
120
+ /** Buffer records and deliver in batches instead of synchronously. */
121
+ batch: boolean;
122
+ /** Batch size before an automatic flush (only when `batch`). */
123
+ batchSize: number;
124
+ /** Default sample rate when config omits `sampleRate`. */
125
+ sampleRate: number;
126
+ /** Pretty, single-line console output (vs. structured JSON). */
127
+ pretty: boolean;
128
+ /**
129
+ * Flush buffered records on `pagehide` + `visibilitychange` (hidden),
130
+ * using `navigator.sendBeacon` when available.
131
+ */
132
+ beaconFlush: boolean;
133
+ }
134
+ /**
135
+ * - development: sync, pretty, level=debug, no batching, sample everything.
136
+ * - production: batched + sampled, level>=warn, beacon flush on page exit.
137
+ */
138
+ declare const PRESETS: Record<TelemetryEnv, TelemetryPreset>;
139
+ /** Resolve the preset for an env (defensive copy so callers can't mutate it). */
140
+ declare function resolvePreset(env: TelemetryEnv): TelemetryPreset;
141
+
142
+ /**
143
+ * createTelemetry — creates a telemetry manager that fans records out to
144
+ * registered sinks. Manager/provider pattern, mirroring `createAI`.
145
+ *
146
+ * - `enabled: false` -> tree-shakeable noop (zero emissions, no engines).
147
+ * - no `endpoint` -> console-only transport.
148
+ * - `endpoint` set -> async Faro engine is registered when the optional
149
+ * peers exist; console stays as a safe fallback until then.
150
+ *
151
+ * The returned object IS a logger (root context = `{}`); `child()` derives
152
+ * loggers with bound context (sessionId / interviewId / turnId / ...).
153
+ */
154
+ declare function createTelemetry(config: TelemetryConfig): Telemetry;
155
+
156
+ interface ConsoleSinkOptions {
157
+ /** Single-line pretty output (vs. structured JSON). */
158
+ pretty?: boolean;
159
+ /** Console to write to (injectable for tests). Defaults to global console. */
160
+ console?: Pick<Console, 'debug' | 'info' | 'warn' | 'error'>;
161
+ }
162
+ /**
163
+ * Default zero-dependency transport. Used whenever no `endpoint` is set.
164
+ * Synchronous; `flush()` is a resolved no-op (nothing is buffered).
165
+ */
166
+ declare function createConsoleSink(opts?: ConsoleSinkOptions): TelemetrySink;
167
+
168
+ /**
169
+ * Faro-backed engine. `@grafana/faro-web-sdk` + `@grafana/faro-web-tracing`
170
+ * are **optional peerDependencies** — they are loaded dynamically and never
171
+ * referenced in this module's public types. If the peers are absent the
172
+ * factory resolves to `null` so the caller can fall back to console.
173
+ *
174
+ * For tests, a `transport` may be injected: an object with a `push(payload)`
175
+ * method. This bypasses Faro entirely (no network, no peer required).
176
+ */
177
+ /** Minimal structural shape of a Faro-ish transport. Not exported. */
178
+ interface FaroTransport {
179
+ push(payload: {
180
+ kind: 'log' | 'span';
181
+ record: LogRecord | SpanRecord;
182
+ }): void;
183
+ }
184
+ interface FaroEngineOptions {
185
+ app: string;
186
+ endpoint: string;
187
+ /**
188
+ * Test/override transport. When provided, the Faro peers are NOT loaded
189
+ * and records are forwarded straight to `transport.push`.
190
+ */
191
+ transport?: FaroTransport;
192
+ }
193
+ /**
194
+ * Construct the Faro engine. Returns `null` when the optional peers are not
195
+ * installed and no override transport was supplied — callers treat `null` as
196
+ * "fall back to console".
197
+ */
198
+ declare function createFaroSink(opts: FaroEngineOptions): Promise<TelemetrySink | null>;
199
+
200
+ /**
201
+ * Returned by {@link createTelemetry} when `enabled: false`. Every method is
202
+ * an empty stub, so a bundler can dead-code-eliminate call sites and the
203
+ * engines (console/Faro) are never imported at runtime. Zero emissions.
204
+ */
205
+ declare function createNoopTelemetry(): Telemetry;
206
+
207
+ /**
208
+ * Deep-strip any object key whose name (case-insensitive) is in `keys`.
209
+ * Arrays are walked; cycles are guarded; non-matching values pass through
210
+ * unchanged. Returns a new structure — the input is never mutated.
211
+ */
212
+ declare function redact(value: LogContext, keys: string[]): LogContext;
213
+
214
+ interface MockSinkExtended extends TelemetrySink {
215
+ /** Every log record received, in order. */
216
+ logs: LogRecord[];
217
+ /** Every span record received, in order. */
218
+ spans: SpanRecord[];
219
+ /** Number of times {@link TelemetrySink.flush} was called. */
220
+ flushCalls: number;
221
+ }
222
+ /**
223
+ * createMockSink — a {@link TelemetrySink} that records everything for
224
+ * assertions instead of doing I/O. Used to test the manager and the Faro
225
+ * engine without any network. Mirrors `createMockAIProvider` in `packages/ai`.
226
+ */
227
+ declare function createMockSink(name?: string): MockSinkExtended;
228
+
229
+ export { type ConsoleSinkOptions, type FaroEngineOptions, LEVEL_ORDER, type LogContext, type LogLevel, type LogRecord, type Logger, type MockSinkExtended, PRESETS, type Span, type SpanRecord, type Telemetry, type TelemetryConfig, type TelemetryEnv, type TelemetryPreset, type TelemetrySink, createConsoleSink, createFaroSink, createMockSink, createNoopTelemetry, createTelemetry, redact, resolvePreset };