@drakkar.software/sunglasses-core 0.2.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,1093 @@
1
+ /**
2
+ * Platform-agnostic key-value storage interface.
3
+ * Implementations: LocalStorageAdapter (web), AsyncStorageAdapter (RN).
4
+ */
5
+ interface IStorageAdapter {
6
+ read(key: string): Promise<string | null>;
7
+ write(key: string, value: string): Promise<void>;
8
+ delete(key: string): Promise<void>;
9
+ /** Optional: flush in-flight writes (useful for HTTP-backed stores). */
10
+ flush?(): Promise<void>;
11
+ }
12
+ /**
13
+ * Output destination that receives batches of sanitized, consented events.
14
+ * Implementations: HttpStorageAdapter, StarfishAnalyticsAdapter, console (debug).
15
+ */
16
+ interface IAnalyticsAdapter {
17
+ /**
18
+ * Deliver a batch of events. Must NOT mutate the input array.
19
+ * The array is `ReadonlyArray` to enforce this at the type level;
20
+ * at runtime it is also `Object.freeze`'d by `SunglassesCore`.
21
+ */
22
+ send(batch: ReadonlyArray<SunglassesEvent>): Promise<void>;
23
+ /** Called on identity reset — adapter may clear remote session. */
24
+ reset?(): Promise<void>;
25
+ /** Called on SDK shutdown — adapter should flush pending work. */
26
+ shutdown?(): Promise<void>;
27
+ /**
28
+ * Called after a successful flush with the events that were delivered.
29
+ * Use this to archive or remove old events from the remote store.
30
+ * Implement this in adapters that accumulate data (e.g. StarfishAnalyticsAdapter).
31
+ */
32
+ cleanupAfterFlush?(delivered: ReadonlyArray<SunglassesEvent>, config: CleanupConfig): Promise<void>;
33
+ }
34
+ /**
35
+ * Controls what happens to delivered events in the remote store after a
36
+ * successful flush. Adapters that support `cleanupAfterFlush` will use this
37
+ * to decide which events to archive or remove.
38
+ */
39
+ interface CleanupConfig {
40
+ /** Remove events older than this many milliseconds. Defaults to 30 days. */
41
+ maxAgeMs?: number;
42
+ /**
43
+ * Keep only the most recent N events per identity.
44
+ * Applied after `maxAgeMs`. Set to 0 to disable.
45
+ */
46
+ maxEventsPerIdentity?: number;
47
+ }
48
+ type MiddlewareNext = (event: SunglassesEvent) => Promise<SunglassesEvent | null>;
49
+ /**
50
+ * Middleware that can transform or drop events before they are queued.
51
+ * Return `null` to silently drop the event. Must never throw.
52
+ */
53
+ interface IMiddleware {
54
+ name: string;
55
+ process(event: SunglassesEvent, next: MiddlewareNext): Promise<SunglassesEvent | null>;
56
+ }
57
+ type EventType = 'capture' | 'screen' | 'identify' | 'alias' | 'group';
58
+ interface EventContext {
59
+ library: {
60
+ name: string;
61
+ version: string;
62
+ };
63
+ platform: 'web' | 'react-native';
64
+ app?: {
65
+ name?: string;
66
+ version?: string;
67
+ build?: string;
68
+ };
69
+ device?: {
70
+ type?: string;
71
+ os?: string;
72
+ };
73
+ screen?: {
74
+ width?: number;
75
+ height?: number;
76
+ };
77
+ locale?: string;
78
+ /** Current session ID. Present when enableSessionTracking is true. */
79
+ sessionId?: string;
80
+ /** Persisted user traits set via identify(). Forwarded to backends for segmentation. */
81
+ traits?: Record<string, unknown>;
82
+ /** Group identity set via group(). Enables organization-level segmentation. */
83
+ group?: {
84
+ id: string;
85
+ };
86
+ }
87
+ /**
88
+ * Canonical event shape sent to IAnalyticsAdapter implementations.
89
+ */
90
+ interface SunglassesEvent {
91
+ type: EventType;
92
+ /** Human-readable event name, e.g. 'Button Clicked', '$screen', '$identify'. */
93
+ event: string;
94
+ /** Resolved user ID (hashed if anonymizeUserId=true). Falls back to anonymousId. */
95
+ distinctId: string;
96
+ /** Stable device UUID — never contains PII, regenerated only on reset(). */
97
+ anonymousId: string;
98
+ /** ISO-8601 UTC timestamp. */
99
+ timestamp: string;
100
+ /** UUID v4 per event — used for de-duplication. */
101
+ messageId: string;
102
+ properties: Record<string, unknown>;
103
+ context: EventContext;
104
+ }
105
+ interface IdentityState {
106
+ anonymousId: string;
107
+ /** null until identify() is called */
108
+ distinctId: string | null;
109
+ isIdentified: boolean;
110
+ }
111
+ type ConsentStatus = 'opted-in' | 'opted-out' | 'unknown';
112
+ /**
113
+ * A single entry in the consent audit trail.
114
+ */
115
+ interface ConsentHistoryEntry {
116
+ status: ConsentStatus;
117
+ /** Privacy policy version in effect when this change was recorded. */
118
+ policyVersion?: string;
119
+ timestamp: string;
120
+ }
121
+ interface ConsentState {
122
+ status: ConsentStatus;
123
+ /** ISO-8601 timestamp of last status change, or null on first run. */
124
+ updatedAt: string | null;
125
+ /** Policy version when consent was last given/revoked. */
126
+ policyVersion?: string;
127
+ /** Audit trail of consent changes. Capped at 10 entries. */
128
+ history?: ConsentHistoryEntry[];
129
+ }
130
+ interface SunglassesConfig {
131
+ /** At least one output adapter is required. */
132
+ adapters: IAnalyticsAdapter[];
133
+ /** Local persistence adapter (platform-specific). */
134
+ storage: IStorageAdapter;
135
+ /**
136
+ * When false (default), the SDK starts in opted-out state.
137
+ * The user must explicitly call optIn() before any events are tracked.
138
+ * Set to true for analytics-first flows where consent is obtained externally.
139
+ */
140
+ defaultOptIn?: boolean;
141
+ /** If set, only these property keys are kept. All others are stripped. */
142
+ allowedProperties?: string[];
143
+ /** Property keys that are always stripped (lower precedence than allowedProperties). */
144
+ deniedProperties?: string[];
145
+ /**
146
+ * When true, distinctId is SHA-256 hashed before being included in events.
147
+ * The raw ID is never stored in the event payload.
148
+ */
149
+ anonymizeUserId?: boolean;
150
+ /**
151
+ * When true (default), the SDK checks `navigator.globalPrivacyControl` (GPC)
152
+ * and `navigator.doNotTrack` (DNT) during initialization. If either signal is
153
+ * detected and the user has not yet made an explicit in-app consent choice,
154
+ * the SDK automatically calls `optOut()`.
155
+ *
156
+ * GPC is legally binding under CPRA (California). DNT is advisory.
157
+ *
158
+ * Set to `false` to disable this behavior — for example, if your consent UI
159
+ * already handles opt-out separately.
160
+ *
161
+ * Only applies on the `'web'` platform. No-op on `'react-native'`.
162
+ */
163
+ respectDoNotTrack?: boolean;
164
+ /** Auto-flush interval in ms. Default: 30_000. */
165
+ flushInterval?: number;
166
+ /** Max events held in the in-memory + persisted queue. Default: 500. */
167
+ maxQueueSize?: number;
168
+ /** Max events per adapter.send() call. Default: 50. */
169
+ maxBatchSize?: number;
170
+ platform?: 'web' | 'react-native';
171
+ appName?: string;
172
+ appVersion?: string;
173
+ appBuild?: string;
174
+ /** Enables verbose console logging. Never enable in production. */
175
+ debug?: boolean;
176
+ /** Hard-disables all tracking (e.g. CI environments, test suites). */
177
+ disabled?: boolean;
178
+ /** Additional middleware appended after the built-in PiiSanitizer. */
179
+ middleware?: IMiddleware[];
180
+ /**
181
+ * When set, calls `adapter.cleanupAfterFlush()` after every successful flush.
182
+ * Only adapters that implement `cleanupAfterFlush` will respond.
183
+ * Useful for pruning old events from Starfish documents or remote stores.
184
+ */
185
+ cleanupAfterFlush?: CleanupConfig;
186
+ /**
187
+ * When true, event counts are tracked in storage (see EventCounter).
188
+ * The FrequencyMiddleware must be added to `middleware` to attach counts
189
+ * to events automatically.
190
+ */
191
+ enableEventCounting?: boolean;
192
+ /**
193
+ * When true, a session ID is generated and attached to every event's context.
194
+ * Sessions expire after `sessionIdleTimeoutMs` of inactivity.
195
+ */
196
+ enableSessionTracking?: boolean;
197
+ /** Idle timeout before a new session is started. Default: 1_800_000 (30 min). */
198
+ sessionIdleTimeoutMs?: number;
199
+ /**
200
+ * If provided, the SDK checks whether stored consent was given for this policy
201
+ * version. If the stored version differs, consent is reset to 'unknown' so the
202
+ * user is prompted again. Useful when your privacy policy changes.
203
+ */
204
+ consentPolicyVersion?: string;
205
+ /**
206
+ * If set, consent older than this many milliseconds is automatically reset to
207
+ * 'unknown', prompting the user to consent again. Useful for regulatory
208
+ * compliance that requires re-obtaining consent periodically.
209
+ *
210
+ * Example: `365 * 24 * 60 * 60 * 1000` re-asks every year.
211
+ *
212
+ * The expiry check runs on SDK initialization. It does not override an
213
+ * explicit in-session `optIn()` or `optOut()` call.
214
+ */
215
+ consentExpiryMs?: number;
216
+ /**
217
+ * When true, every event that passes the middleware pipeline is also written
218
+ * to a permanent local archive (`sunglasses:archive` in IStorageAdapter).
219
+ *
220
+ * Unlike the EventQueue, the archive is **never cleared automatically** after
221
+ * a flush — events persist until `client.clearLocalArchive()` is called.
222
+ *
223
+ * Use cases:
224
+ * - Keep a full local history for GDPR data portability
225
+ * - Re-sync a remote store from scratch after a failure
226
+ * - Offline-first: accumulate events even if all adapters are down
227
+ */
228
+ enableLocalArchive?: boolean;
229
+ }
230
+ /**
231
+ * The main SDK surface. Implemented by SunglassesCore.
232
+ * Both @drakkar.software/sunglasses-react and @drakkar.software/sunglasses-react-native expose this via Context.
233
+ */
234
+ interface ISunglassesClient {
235
+ /**
236
+ * Track a custom event. Silently dropped when opted-out or disabled.
237
+ * @param options - Optional overrides for timestamp and messageId.
238
+ */
239
+ capture(eventName: string, properties?: Record<string, unknown>, options?: CaptureOptions): void;
240
+ /** Track a screen/page view. */
241
+ screen(screenName: string, properties?: Record<string, unknown>): void;
242
+ /** Link the current anonymous session to a known user. */
243
+ identify(userId: string, traits?: Record<string, unknown>): void;
244
+ /** Create an alias between two identities (e.g. pre/post login merge). */
245
+ alias(newId: string, existingId: string): void;
246
+ /**
247
+ * Associate the current user with a group (e.g. organisation, workspace, team).
248
+ * Emits a `group` event and attaches the group ID to all subsequent events via
249
+ * `context.group`. Silently dropped when opted-out or disabled.
250
+ *
251
+ * Group identity is in-memory only and must be re-set after `reset()` or restart.
252
+ */
253
+ group(groupId: string, groupTraits?: Record<string, unknown>): void;
254
+ /** Clear identity and generate a fresh anonymous ID. */
255
+ reset(): Promise<void>;
256
+ /**
257
+ * Register properties that are automatically merged into every subsequent event.
258
+ * Per-event properties passed to `capture()` override registered properties with
259
+ * the same key. Use for non-PII session/environment metadata such as
260
+ * `{ environment: 'production', experiment_group: 'A' }`.
261
+ *
262
+ * Registered properties are in-memory only and must be re-registered on restart.
263
+ */
264
+ register(properties: Record<string, unknown>): void;
265
+ /**
266
+ * Remove specific keys from the registered super properties.
267
+ * If called with no arguments, all registered properties are cleared.
268
+ */
269
+ unregister(...keys: string[]): void;
270
+ /** Returns a snapshot of all currently registered super properties. */
271
+ getRegisteredProperties(): Record<string, unknown>;
272
+ optIn(): Promise<void>;
273
+ optOut(): Promise<void>;
274
+ hasOptedIn(): boolean;
275
+ hasOptedOut(): boolean;
276
+ getConsentStatus(): ConsentStatus;
277
+ /** Force-flush the event queue to all adapters immediately. */
278
+ flush(): Promise<void>;
279
+ /** Flush and tear down timers. Call on app/component unmount. */
280
+ shutdown(): Promise<void>;
281
+ /**
282
+ * Get the number of times an event has been fired in a given period.
283
+ * Returns 0 if event counting is disabled or no data exists.
284
+ */
285
+ getEventCount(eventName: string, period: EventCountPeriod, date?: Date): Promise<number>;
286
+ /**
287
+ * Reset the count for a specific event (or all events if omitted).
288
+ */
289
+ resetEventCount(eventName?: string): Promise<void>;
290
+ /** Access the underlying EventCounter for advanced usage. */
291
+ readonly eventCounter: IEventCounter | null;
292
+ /** Expose the current queue length (e.g. for UI indicators). */
293
+ getQueuedEventCount(): number;
294
+ /** Returns the audit trail of consent changes (most recent last). */
295
+ getConsentHistory(): ConsentHistoryEntry[];
296
+ /**
297
+ * Prune archived events by age / count, or clear them entirely.
298
+ * No-op if `enableLocalArchive` was not set in config.
299
+ *
300
+ * @param config — pass an empty object `{}` to clear everything
301
+ */
302
+ clearLocalArchive(config?: CleanupConfig): Promise<void>;
303
+ /**
304
+ * Export all locally held user data as a structured object.
305
+ * GDPR Article 20 — right to data portability.
306
+ * No network calls — reads only from in-memory subsystem state.
307
+ */
308
+ exportUserData(): Promise<UserDataExport>;
309
+ /**
310
+ * Erase all locally held user data.
311
+ * GDPR Article 17 — right to erasure.
312
+ *
313
+ * Clears: event queue, user traits, session, event counts, local archive.
314
+ * Resets identity to a fresh anonymous ID (new UUID, no distinctId).
315
+ * Clears in-memory super properties and group identity.
316
+ * Calls `adapter.reset()` on all adapters.
317
+ *
318
+ * @param options.resetConsent - When true, also resets consent status to
319
+ * 'unknown' so the user is prompted to consent again. Defaults to false
320
+ * because the consent audit trail is evidence of past user choices and
321
+ * may have regulatory significance.
322
+ */
323
+ deleteUserData(options?: {
324
+ resetConsent?: boolean;
325
+ }): Promise<void>;
326
+ }
327
+ /** Time granularity for event frequency tracking. */
328
+ type EventCountPeriod = 'daily' | 'weekly' | 'monthly' | 'all-time';
329
+ /**
330
+ * Tracks how many times each event has been fired, bucketed by time period.
331
+ * Counts are persisted to IStorageAdapter and survive app restarts.
332
+ */
333
+ interface IEventCounter {
334
+ /**
335
+ * Increment the count for an event on a given date (defaults to now).
336
+ */
337
+ increment(eventName: string, date?: Date): Promise<void>;
338
+ /**
339
+ * Get the count for an event in a period.
340
+ * - 'daily': count for the calendar day containing `date`
341
+ * - 'weekly': count for the ISO week containing `date` (Mon–Sun)
342
+ * - 'monthly': count for the calendar month containing `date`
343
+ * - 'all-time': total count across all time
344
+ */
345
+ getCount(eventName: string, period: EventCountPeriod, date?: Date): Promise<number>;
346
+ /**
347
+ * Reset the count for a specific event, or all events if `eventName` is omitted.
348
+ */
349
+ reset(eventName?: string): Promise<void>;
350
+ }
351
+ interface HttpAdapterConfig {
352
+ /** Full URL of the ingest endpoint, e.g. https://analytics.example.com/batch */
353
+ endpoint: string;
354
+ /** Extra request headers, e.g. { 'X-API-Key': '...' } */
355
+ headers?: Record<string, string>;
356
+ /** Events per POST request. Default: 50. */
357
+ batchSize?: number;
358
+ /** Auto-flush interval in ms. Default: 30_000. */
359
+ flushIntervalMs?: number;
360
+ /** Maximum retry attempts before discarding a batch. Default: 3. */
361
+ maxRetries?: number;
362
+ /** Initial retry delay in ms (doubles each attempt). Default: 1_000. */
363
+ retryBaseDelayMs?: number;
364
+ /** Max retry delay cap in ms. Default: 30_000. */
365
+ retryMaxDelayMs?: number;
366
+ /** Request timeout in ms. Default: 10_000. */
367
+ timeout?: number;
368
+ }
369
+ interface StarfishAdapterConfig {
370
+ /** Base URL of the Starfish sync server, e.g. https://sync.example.com */
371
+ serverUrl: string;
372
+ /**
373
+ * Path template for the event document.
374
+ * Use `{identity}` as a placeholder — it is replaced with `distinctId ?? anonymousId`.
375
+ * Example: "analytics/{identity}/events"
376
+ */
377
+ storagePath: string;
378
+ /** Bearer token for Authorization header. */
379
+ authToken?: string;
380
+ /** Max retries on 409 Conflict (optimistic locking). Default: 3. */
381
+ maxRetries?: number;
382
+ /**
383
+ * When true, each successful push creates a **new** Starfish document using
384
+ * a rotating path suffix (e.g. `events-0001`, `events-0002`…).
385
+ *
386
+ * Benefits:
387
+ * - No pull step needed — each push is always a fresh document
388
+ * - No growing single document — each file stays small
389
+ * - Old documents accumulate on Starfish (combine with `cleanupAfterFlush` to prune)
390
+ *
391
+ * Requires `pathStorage` to persist the current path generation counter.
392
+ * Works best with `enableLocalArchive: true` in `SunglassesConfig` so the
393
+ * complete event history is kept locally even across many push generations.
394
+ */
395
+ rotatePathOnSuccess?: boolean;
396
+ /**
397
+ * Storage adapter used to persist the current path generation counter.
398
+ * Required when `rotatePathOnSuccess: true`.
399
+ * Can be the same adapter as `SunglassesConfig.storage`.
400
+ */
401
+ pathStorage?: IStorageAdapter;
402
+ }
403
+ /**
404
+ * In-memory + persisted session state.
405
+ * Session IDs are random UUIDs — they never contain PII.
406
+ */
407
+ interface SessionState {
408
+ sessionId: string;
409
+ startedAt: string;
410
+ lastActiveAt: string;
411
+ eventCount: number;
412
+ }
413
+ /**
414
+ * Machine-readable snapshot of all user data held locally by the SDK.
415
+ * Returned by `client.exportUserData()`.
416
+ */
417
+ interface UserDataExport {
418
+ exportedAt: string;
419
+ anonymousId: string;
420
+ distinctId: string | null;
421
+ consentStatus: ConsentStatus;
422
+ consentHistory: ConsentHistoryEntry[];
423
+ /** Persisted user traits set via identify(). */
424
+ traits: Record<string, unknown>;
425
+ /** Events that have been queued but not yet delivered to any adapter. */
426
+ queuedEvents: SunglassesEvent[];
427
+ /**
428
+ * All events in the local archive.
429
+ * Only populated when `enableLocalArchive: true`.
430
+ */
431
+ archivedEvents: SunglassesEvent[];
432
+ /**
433
+ * Summary of event counts per period.
434
+ * Only populated when `enableEventCounting: true`.
435
+ */
436
+ eventCountSummary: {
437
+ [eventName: string]: Partial<Record<EventCountPeriod, number>>;
438
+ };
439
+ }
440
+ /**
441
+ * Optional overrides for a single `capture()` call.
442
+ * Use sparingly — the defaults (auto timestamp, auto messageId) are correct
443
+ * for the vast majority of use cases.
444
+ */
445
+ interface CaptureOptions {
446
+ /**
447
+ * Override the event timestamp (ISO-8601 UTC string).
448
+ * Useful for back-dating events captured offline or migrating historical data.
449
+ * Defaults to `Date.now()` at the time `capture()` is called.
450
+ */
451
+ timestamp?: string;
452
+ /**
453
+ * Override the deduplication ID (UUID v4).
454
+ * Useful when the event was originally generated server-side and you want
455
+ * to guarantee idempotent delivery even after retries.
456
+ * Defaults to a freshly generated UUID v4.
457
+ */
458
+ messageId?: string;
459
+ }
460
+ /**
461
+ * A map of event name → required properties shape.
462
+ * Use with `asTyped<MyEventMap>(client)` to get compile-time checked `capture()`.
463
+ *
464
+ * @example
465
+ * type MyEvents = {
466
+ * button_clicked: { buttonId: string; screen: string };
467
+ * purchase_completed: { itemId: string; amount: number };
468
+ * page_viewed: undefined; // no required properties
469
+ * };
470
+ */
471
+ type EventMap = Record<string, Record<string, unknown> | undefined>;
472
+ /**
473
+ * Typed wrapper around ISunglassesClient.
474
+ * Provides compile-time checking of event names and their property shapes.
475
+ * Zero runtime cost — use `asTyped<T>(client)` to obtain one.
476
+ */
477
+ interface ISunglassesTypedClient<T extends EventMap> extends ISunglassesClient {
478
+ capture<K extends keyof T & string>(eventName: K, ...args: T[K] extends undefined ? [properties?: Record<string, unknown>] : [properties: T[K]]): void;
479
+ }
480
+ interface ScreenTrackingOptions {
481
+ /** Web: listen to history.pushState / replaceState / popstate. Default: true. */
482
+ useHistoryApi?: boolean;
483
+ /** RN + Expo Router: auto-listen to usePathname changes. */
484
+ useExpoRouter?: boolean;
485
+ /** RN + React Navigation: pass the NavigationContainerRef. */
486
+ navigationRef?: unknown;
487
+ /** Transform raw pathname/route name → human-readable screen name. */
488
+ screenNameMapper?: (path: string) => string;
489
+ }
490
+
491
+ /**
492
+ * Core SDK engine. Platform-agnostic implementation of ISunglassesClient.
493
+ *
494
+ * Use SunglassesCore.create(config) to get an initialized instance.
495
+ * Do not construct directly — initialization is async and must complete
496
+ * before any events are captured.
497
+ */
498
+ declare class SunglassesCore implements ISunglassesClient {
499
+ private readonly consent;
500
+ private readonly identity;
501
+ private readonly queue;
502
+ private readonly pipeline;
503
+ private readonly adapters;
504
+ private readonly _eventCounter;
505
+ private readonly cleanupConfig;
506
+ private readonly sessionManager;
507
+ private readonly traitManager;
508
+ private readonly localArchive;
509
+ private readonly config;
510
+ private flushTimer;
511
+ private isShutdown;
512
+ /** Guard against concurrent flushes — prevents double-send. */
513
+ private flushInFlight;
514
+ /** In-memory super properties merged into every event's properties. */
515
+ private readonly superProperties;
516
+ /** In-memory group ID attached to every event's context after group() is called. */
517
+ private groupId;
518
+ private constructor();
519
+ /**
520
+ * Create and initialize a SunglassesCore instance.
521
+ * This is the only way to construct the SDK.
522
+ */
523
+ static create(config: SunglassesConfig): Promise<SunglassesCore>;
524
+ capture(eventName: string, properties?: Record<string, unknown>, options?: CaptureOptions): void;
525
+ screen(screenName: string, properties?: Record<string, unknown>): void;
526
+ identify(userId: string, traits?: Record<string, unknown>): void;
527
+ alias(newId: string, existingId: string): void;
528
+ group(groupId: string, groupTraits?: Record<string, unknown>): void;
529
+ register(properties: Record<string, unknown>): void;
530
+ unregister(...keys: string[]): void;
531
+ getRegisteredProperties(): Record<string, unknown>;
532
+ reset(): Promise<void>;
533
+ optIn(): Promise<void>;
534
+ optOut(): Promise<void>;
535
+ hasOptedIn(): boolean;
536
+ hasOptedOut(): boolean;
537
+ getConsentStatus(): ConsentStatus;
538
+ getConsentHistory(): ConsentHistoryEntry[];
539
+ flush(): Promise<void>;
540
+ shutdown(): Promise<void>;
541
+ get eventCounter(): IEventCounter | null;
542
+ getEventCount(eventName: string, period: EventCountPeriod, date?: Date): Promise<number>;
543
+ resetEventCount(eventName?: string): Promise<void>;
544
+ getQueuedEventCount(): number;
545
+ /**
546
+ * Prune the local event archive by age/count, or clear it entirely.
547
+ * Pass `{}` to clear all archived events.
548
+ * No-op when `enableLocalArchive` was not set in config.
549
+ */
550
+ clearLocalArchive(config?: CleanupConfig): Promise<void>;
551
+ /**
552
+ * Export all locally held user data as a machine-readable object.
553
+ * GDPR Article 20 — right to data portability.
554
+ */
555
+ exportUserData(): Promise<UserDataExport>;
556
+ /**
557
+ * Erase all locally held user data. GDPR Article 17 — right to erasure.
558
+ */
559
+ deleteUserData(options?: {
560
+ resetConsent?: boolean;
561
+ }): Promise<void>;
562
+ private canCapture;
563
+ private enqueueEvent;
564
+ private emitSessionStart;
565
+ private buildContext;
566
+ /**
567
+ * Flush up to maxBatchSize events to every adapter.
568
+ *
569
+ * Guards:
570
+ * - If a flush is already in flight, returns immediately (no double-send).
571
+ * - If ALL adapters succeed, events are removed from the queue.
572
+ * - If ANY adapter fails, events stay in the queue for the next flush attempt.
573
+ * The failed adapter's own retry logic handles re-delivery.
574
+ */
575
+ private flushOnce;
576
+ private startFlushTimer;
577
+ private stopFlushTimer;
578
+ }
579
+
580
+ /**
581
+ * Built-in middleware that sanitizes event properties before they are queued.
582
+ *
583
+ * Sanitization order (allowedProperties takes precedence over deniedProperties):
584
+ * 1. If allowedProperties is set: keep only those top-level keys.
585
+ * 2. Else if deniedProperties is set: remove those top-level keys.
586
+ * 3. Strip built-in PII key names (BUILTIN_DENIED_KEYS) at every nesting level.
587
+ * 4. Recursively traverse objects and arrays; redact string values matching PII_PATTERNS.
588
+ *
589
+ * This middleware is always prepended to the pipeline by SunglassesCore.
590
+ * It must never throw.
591
+ */
592
+ declare class PiiSanitizer implements IMiddleware {
593
+ private readonly allowedProperties?;
594
+ private readonly deniedProperties?;
595
+ readonly name = "PiiSanitizer";
596
+ constructor(allowedProperties?: string[] | undefined, deniedProperties?: string[] | undefined);
597
+ process(event: SunglassesEvent, next: MiddlewareNext): Promise<SunglassesEvent | null>;
598
+ private sanitizeProperties;
599
+ /**
600
+ * Recursively traverse an object or array:
601
+ * - Remove keys in BUILTIN_DENIED_KEYS (case-insensitive)
602
+ * - Redact string values that contain PII
603
+ * - Recurse into nested objects and arrays
604
+ */
605
+ private deepSanitizeValues;
606
+ private containsPii;
607
+ }
608
+
609
+ interface Logger {
610
+ debug(...args: unknown[]): void;
611
+ info(...args: unknown[]): void;
612
+ warn(...args: unknown[]): void;
613
+ error(...args: unknown[]): void;
614
+ }
615
+ /**
616
+ * Create a logger that respects the `debug` config flag.
617
+ * debug/info are no-ops when debug=false.
618
+ * warn/error always emit.
619
+ */
620
+ declare function createLogger(debug: boolean): Logger;
621
+
622
+ /**
623
+ * Executes an ordered chain of IMiddleware instances.
624
+ *
625
+ * - Middleware runs in array order.
626
+ * - If any middleware returns null, the event is dropped (no further processing).
627
+ * - Errors inside middleware are caught and treated as a drop (logged as error).
628
+ */
629
+ declare class MiddlewarePipeline {
630
+ private readonly middleware;
631
+ private readonly logger;
632
+ constructor(middleware: IMiddleware[], logger: Logger);
633
+ run(event: SunglassesEvent): Promise<SunglassesEvent | null>;
634
+ }
635
+
636
+ /**
637
+ * Persists per-event counts bucketed by time period (daily, weekly, monthly, all-time).
638
+ *
639
+ * Storage key format:
640
+ * `sunglasses:count:{period}:{bucket}:{eventName}`
641
+ *
642
+ * where `bucket` is:
643
+ * - daily: "2024-01-15"
644
+ * - weekly: "2024-W03" (ISO week)
645
+ * - monthly: "2024-01"
646
+ * - all-time: "all"
647
+ *
648
+ * Counts survive app restarts (persisted to IStorageAdapter).
649
+ * Designed to be written from the enqueue hot path — must not throw.
650
+ */
651
+ declare class EventCounter implements IEventCounter {
652
+ private readonly storage;
653
+ private readonly logger;
654
+ /**
655
+ * In-memory cache of counts keyed by storage key.
656
+ * Updated synchronously on increment so getCount() can return immediately
657
+ * without waiting for storage writes to complete.
658
+ */
659
+ private readonly cache;
660
+ /**
661
+ * Tracks all storage keys written during this session.
662
+ * Used to reliably clear an event's timed buckets on reset(),
663
+ * even when they fall outside the 90-day sweep window.
664
+ */
665
+ private readonly writtenKeys;
666
+ constructor(storage: IStorageAdapter, logger: Logger);
667
+ /**
668
+ * Increment the count for `eventName` across all four periods for `date`.
669
+ */
670
+ increment(eventName: string, date?: Date): Promise<void>;
671
+ getCount(eventName: string, period: EventCountPeriod, date?: Date): Promise<number>;
672
+ /**
673
+ * Reset counts for a specific event (all periods), or all events if omitted.
674
+ *
675
+ * Note: resetting all events requires listing storage keys. This is a best-effort
676
+ * operation — adapters that don't support key enumeration will only clear
677
+ * the provided event name's keys.
678
+ */
679
+ reset(eventName?: string): Promise<void>;
680
+ private incrementPeriod;
681
+ private clearEvent;
682
+ /** Build the storage key for a given event, period, and date. */
683
+ storageKey(eventName: string, period: EventCountPeriod, date: Date): string;
684
+ /**
685
+ * Return the time-bucket string for a given period and date.
686
+ * - daily: "2024-01-15"
687
+ * - weekly: "2024-W03"
688
+ * - monthly: "2024-01"
689
+ * - all-time: "all"
690
+ */
691
+ private bucketFor;
692
+ }
693
+
694
+ interface FrequencyMiddlewareOptions {
695
+ /**
696
+ * The EventCounter instance to read from.
697
+ * Obtain this from `SunglassesCore.eventCounter` after calling create()
698
+ * with `enableEventCounting: true`.
699
+ */
700
+ counter: EventCounter;
701
+ /**
702
+ * Which periods to attach to event properties. Default: ['daily', 'monthly'].
703
+ * Each selected period adds a property like `$count_daily` or `$count_monthly`.
704
+ */
705
+ periods?: EventCountPeriod[];
706
+ /**
707
+ * Only attach count properties for these event names.
708
+ * When omitted, counts are attached to all `capture` events.
709
+ */
710
+ onlyFor?: string[];
711
+ }
712
+ /**
713
+ * Optional middleware that reads event counts from EventCounter and attaches
714
+ * them to outgoing event properties.
715
+ *
716
+ * Properties added (example with periods: ['daily', 'monthly', 'all-time']):
717
+ * ```json
718
+ * {
719
+ * "$count_daily": 3,
720
+ * "$count_monthly": 12,
721
+ * "$count_all_time": 47
722
+ * }
723
+ * ```
724
+ *
725
+ * Usage:
726
+ * ```ts
727
+ * const counter = core.eventCounter!;
728
+ * const freq = new FrequencyMiddleware({ counter, periods: ['daily', 'monthly'] });
729
+ *
730
+ * // Pass at creation time:
731
+ * const client = await SunglassesCore.create({
732
+ * enableEventCounting: true,
733
+ * middleware: [freq],
734
+ * ...
735
+ * });
736
+ * ```
737
+ *
738
+ * Note: counts reflect the state BEFORE this event is counted. EventCounter.increment
739
+ * is called from SunglassesCore AFTER the middleware pipeline completes, so the
740
+ * attached count does NOT yet include the current event (e.g. a `$count_all_time`
741
+ * of 4 means this is the 5th occurrence — the counter will be 5 after enqueue).
742
+ */
743
+ declare class FrequencyMiddleware implements IMiddleware {
744
+ readonly name = "FrequencyMiddleware";
745
+ private readonly counter;
746
+ private readonly periods;
747
+ private readonly onlyFor;
748
+ constructor(options: FrequencyMiddlewareOptions);
749
+ process(event: SunglassesEvent, next: MiddlewareNext): Promise<SunglassesEvent | null>;
750
+ }
751
+
752
+ interface SamplingMiddlewareOptions {
753
+ /**
754
+ * Fraction of events to keep, between 0 (drop all) and 1 (keep all).
755
+ * Example: 0.1 keeps ~10% of events.
756
+ */
757
+ sampleRate: number;
758
+ /**
759
+ * Only apply sampling to these event names.
760
+ * When omitted, all `capture` events are subject to sampling.
761
+ * `$screen`, `$identify`, and `$alias` events are never sampled (always kept).
762
+ */
763
+ onlyFor?: string[];
764
+ /**
765
+ * When true, the sampling decision is consistent per user session
766
+ * (based on anonymousId hash), so the same user is always included
767
+ * or always excluded. Default: false (random per event).
768
+ */
769
+ consistentSampling?: boolean;
770
+ }
771
+ /**
772
+ * Middleware that randomly drops a fraction of events to reduce analytics volume.
773
+ *
774
+ * Usage:
775
+ * ```ts
776
+ * const sampling = new SamplingMiddleware({ sampleRate: 0.1 }); // Keep 10%
777
+ *
778
+ * const client = await SunglassesCore.create({
779
+ * middleware: [sampling],
780
+ * ...
781
+ * });
782
+ * ```
783
+ *
784
+ * Privacy note: because sampling is done client-side, opted-out users are
785
+ * never sampled (they never reach this middleware).
786
+ */
787
+ declare class SamplingMiddleware implements IMiddleware {
788
+ readonly name = "SamplingMiddleware";
789
+ private readonly sampleRate;
790
+ private readonly onlyFor;
791
+ private readonly consistentSampling;
792
+ constructor(options: SamplingMiddlewareOptions);
793
+ process(event: SunglassesEvent, next: MiddlewareNext): Promise<SunglassesEvent | null>;
794
+ /**
795
+ * Determine inclusion based on anonymousId hash.
796
+ * Produces a stable 0–1 value for the identity so the decision is consistent
797
+ * across all events in the same session.
798
+ */
799
+ private isIncludedByIdentity;
800
+ }
801
+
802
+ /**
803
+ * Manages user consent state.
804
+ *
805
+ * Consent is persisted via the same IStorageAdapter used by the rest of the SDK,
806
+ * so platform-correct storage is used automatically (localStorage on web,
807
+ * AsyncStorage on React Native).
808
+ *
809
+ * State machine:
810
+ * unknown ──optIn()──▶ opted-in
811
+ * unknown ──optOut()──▶ opted-out
812
+ * opted-in ──optOut()──▶ opted-out
813
+ * opted-out ──optIn()──▶ opted-in
814
+ *
815
+ * Policy versioning:
816
+ * When `policyVersion` is provided to `initialize()` and differs from the
817
+ * stored version, the consent status is reset to 'unknown' — prompting the
818
+ * user to consent again under the new policy.
819
+ */
820
+ declare class ConsentManager {
821
+ private readonly storage;
822
+ private readonly logger;
823
+ private state;
824
+ constructor(storage: IStorageAdapter, logger: Logger);
825
+ /**
826
+ * Load persisted consent state. Must be called once during SDK initialization.
827
+ * @param defaultOptIn — when no persisted state exists, opt-in (true) or opt-out (false)
828
+ * @param policyVersion — current policy version; if it differs from stored, consent resets
829
+ * @param expiryMs — if set, consent older than this resets to 'unknown'
830
+ */
831
+ initialize(defaultOptIn: boolean, policyVersion?: string, expiryMs?: number): Promise<void>;
832
+ get status(): ConsentStatus;
833
+ isOptedIn(): boolean;
834
+ isOptedOut(): boolean;
835
+ optIn(policyVersion?: string): Promise<void>;
836
+ optOut(policyVersion?: string): Promise<void>;
837
+ /** Returns a copy of the consent audit trail (oldest first). */
838
+ getHistory(): ConsentHistoryEntry[];
839
+ /**
840
+ * Reset consent status to 'unknown'.
841
+ * Used internally by consent expiry and deleteUserData().
842
+ * Appends a history entry so the reset is auditable.
843
+ */
844
+ resetToUnknown(policyVersion?: string): Promise<void>;
845
+ private appendHistory;
846
+ private persist;
847
+ }
848
+
849
+ /**
850
+ * Manages user identity.
851
+ *
852
+ * - anonymousId: stable UUID generated on first run, persisted, never PII.
853
+ * Only regenerated when reset() is called.
854
+ * - distinctId: set by identify(). Optionally hashed with SHA-256.
855
+ * null until identify() is called — events use anonymousId as distinctId.
856
+ */
857
+ declare class IdentityManager {
858
+ private readonly storage;
859
+ private readonly logger;
860
+ private readonly anonymizeUserId;
861
+ private anonymousId;
862
+ private distinctId;
863
+ constructor(storage: IStorageAdapter, logger: Logger, anonymizeUserId: boolean);
864
+ /** Load persisted identity. Must be called once during SDK initialization. */
865
+ initialize(): Promise<void>;
866
+ getState(): IdentityState;
867
+ getAnonymousId(): string;
868
+ /**
869
+ * Resolved identity for use in events.
870
+ * Returns distinctId if set, otherwise anonymousId.
871
+ */
872
+ getEffectiveDistinctId(): string;
873
+ /**
874
+ * Link current session to a known user.
875
+ * @param userId — the raw user identifier (hashed if anonymizeUserId=true)
876
+ * @throws if userId is empty or whitespace-only
877
+ */
878
+ identify(userId: string): Promise<string>;
879
+ /**
880
+ * Clear identity and generate a fresh anonymous ID.
881
+ * Adapters should also call their own reset() if applicable.
882
+ */
883
+ reset(): Promise<void>;
884
+ }
885
+
886
+ /**
887
+ * In-memory event queue with persistence.
888
+ *
889
+ * - Events are held in memory and persisted to IStorageAdapter after every enqueue.
890
+ * - On initialize(), the queue is loaded from storage (survives app restarts).
891
+ * - When the queue exceeds maxSize, the oldest events are dropped (FIFO).
892
+ * - flush() pops up to batchSize events for delivery; callers remove them on success.
893
+ */
894
+ declare class EventQueue {
895
+ private readonly storage;
896
+ private readonly logger;
897
+ private readonly maxSize;
898
+ private queue;
899
+ private persistTimer;
900
+ private persistPending;
901
+ constructor(storage: IStorageAdapter, logger: Logger, maxSize: number);
902
+ /** Load persisted queue. Call once during SDK initialization. */
903
+ initialize(): Promise<void>;
904
+ /** Add an event to the queue. Triggers async persistence (debounced). */
905
+ enqueue(event: SunglassesEvent): void;
906
+ /** Return up to `batchSize` events without removing them. */
907
+ peek(batchSize: number): SunglassesEvent[];
908
+ /** Remove the first `count` events (call after successful adapter.send). */
909
+ remove(count: number): void;
910
+ get size(): number;
911
+ /** Force-persist the queue immediately. */
912
+ persist(): Promise<void>;
913
+ /** Clear queue from memory and storage. */
914
+ clear(): Promise<void>;
915
+ private schedulePersist;
916
+ }
917
+
918
+ /**
919
+ * Manages a lightweight anonymous session.
920
+ *
921
+ * A session is a contiguous period of activity identified by a random UUID.
922
+ * Sessions expire when the user is idle for longer than `idleTimeoutMs`.
923
+ * Session IDs are never derived from PII — they are always fresh UUIDs.
924
+ *
925
+ * Usage:
926
+ * const sm = new SessionManager(storage, logger, 30 * 60_000);
927
+ * await sm.initialize();
928
+ * const { sessionId } = sm.getOrCreate();
929
+ * sm.touch(); // call on every event to reset the idle timer
930
+ */
931
+ declare class SessionManager {
932
+ private readonly storage;
933
+ private readonly logger;
934
+ private readonly idleTimeoutMs;
935
+ private session;
936
+ private idleTimer;
937
+ constructor(storage: IStorageAdapter, logger: Logger, idleTimeoutMs?: number);
938
+ /**
939
+ * Load the previous session from storage. If it has already expired, it is
940
+ * discarded so that `getOrCreate()` will start a fresh one.
941
+ */
942
+ initialize(): Promise<void>;
943
+ /**
944
+ * Return the current session, creating a new one if none exists.
945
+ * A `$session_start` event should be emitted by the caller whenever this
946
+ * method creates a new session (detectable by checking whether the returned
947
+ * `eventCount` is 0).
948
+ */
949
+ getOrCreate(): SessionState;
950
+ /**
951
+ * Record activity — resets the idle expiry timer and updates `lastActiveAt`.
952
+ * Call this after every event is successfully enqueued.
953
+ */
954
+ touch(now?: Date): void;
955
+ /**
956
+ * Explicitly end the current session (e.g. on sign-out or reset).
957
+ * Clears both in-memory state and storage.
958
+ */
959
+ end(): Promise<void>;
960
+ /** The current session ID, or null if no session is active. */
961
+ get sessionId(): string | null;
962
+ private resetIdleTimer;
963
+ private clearIdleTimer;
964
+ private persist;
965
+ }
966
+
967
+ /**
968
+ * Persists user traits set via `identify()` and makes them available for
969
+ * enriching every subsequent event's `context.traits`.
970
+ *
971
+ * Traits are merged (not replaced) on each `setTraits()` call. Keys whose
972
+ * names appear in the PII deny-list are stripped before storage.
973
+ *
974
+ * Traits are forwarded to analytics backends in `event.context.traits` — they
975
+ * do NOT appear in `event.properties` to avoid polluting per-event data.
976
+ */
977
+ declare class TraitManager {
978
+ private readonly storage;
979
+ private readonly logger;
980
+ private traits;
981
+ constructor(storage: IStorageAdapter, logger: Logger);
982
+ /**
983
+ * Load persisted traits from storage. Call once during SDK initialization.
984
+ */
985
+ initialize(): Promise<void>;
986
+ /**
987
+ * Merge new traits into the persisted set.
988
+ * Sensitive keys (email, phone, password, etc.) are stripped silently.
989
+ * Passing `null` as a value removes that key.
990
+ */
991
+ setTraits(traits: Record<string, unknown>): Promise<void>;
992
+ /** Return a shallow copy of the current traits object. */
993
+ getTraits(): Record<string, unknown>;
994
+ /** Remove all stored traits (called on reset()). */
995
+ clearTraits(): Promise<void>;
996
+ private sanitize;
997
+ private persist;
998
+ }
999
+
1000
+ /**
1001
+ * Append-only local event archive.
1002
+ *
1003
+ * Unlike the EventQueue (which removes events after a successful flush),
1004
+ * the LocalEventArchive retains every event indefinitely — until the user
1005
+ * explicitly calls `cleanup()` or `clear()`.
1006
+ *
1007
+ * Use cases:
1008
+ * - Full audit trail of all captured events
1009
+ * - Re-syncing a remote store (e.g. Starfish) after a failure
1010
+ * - GDPR data portability export
1011
+ * - Offline-first: accumulate events when a remote adapter is unreachable
1012
+ *
1013
+ * Enable via `SunglassesConfig.enableLocalArchive: true`.
1014
+ */
1015
+ declare class LocalEventArchive {
1016
+ private readonly storage;
1017
+ private readonly logger;
1018
+ private events;
1019
+ constructor(storage: IStorageAdapter, logger: Logger);
1020
+ /**
1021
+ * Load all previously archived events from storage.
1022
+ * Call once during SDK initialization.
1023
+ */
1024
+ initialize(): Promise<void>;
1025
+ /**
1026
+ * Append events to the archive.
1027
+ * Deduplicates by `messageId` — safe to call multiple times with the same batch.
1028
+ */
1029
+ append(events: SunglassesEvent[]): Promise<void>;
1030
+ /** All archived events (oldest first). */
1031
+ getAll(): SunglassesEvent[];
1032
+ /** Number of archived events. */
1033
+ get size(): number;
1034
+ /**
1035
+ * Prune archived events by age and/or count.
1036
+ * `maxEventsPerIdentity` is applied **per distinctId** — each identity keeps its
1037
+ * most recent N events independently. If neither option is set, nothing is removed.
1038
+ */
1039
+ cleanup(config?: CleanupConfig): Promise<void>;
1040
+ /**
1041
+ * Remove all archived events and clear storage.
1042
+ * This is irreversible.
1043
+ */
1044
+ clear(): Promise<void>;
1045
+ private persist;
1046
+ }
1047
+
1048
+ /**
1049
+ * Zero-cost cast that adds compile-time event typing to any ISunglassesClient.
1050
+ *
1051
+ * Define your event map once, then use it everywhere for type-safe `capture()` calls:
1052
+ *
1053
+ * ```typescript
1054
+ * type MyEvents = {
1055
+ * button_clicked: { buttonId: string; screen: string };
1056
+ * purchase_completed: { itemId: string; amount: number };
1057
+ * page_viewed: undefined; // no required properties
1058
+ * };
1059
+ *
1060
+ * const typed = asTyped<MyEvents>(client);
1061
+ *
1062
+ * // Compile-time checked:
1063
+ * typed.capture('button_clicked', { buttonId: 'cta', screen: 'home' }); // ✓
1064
+ * typed.capture('button_clicked', { wrong: 'key' }); // ✗ type error
1065
+ * typed.capture('unknown_event', {}); // ✗ type error
1066
+ * ```
1067
+ *
1068
+ * This is a pure type-level operation — there is no runtime overhead.
1069
+ * All other client methods (`screen`, `identify`, `flush`, etc.) remain unchanged.
1070
+ */
1071
+ declare function asTyped<T extends EventMap>(client: ISunglassesClient): ISunglassesTypedClient<T>;
1072
+
1073
+ /**
1074
+ * Generate a UUID v4 using the Web Crypto API.
1075
+ *
1076
+ * Works in:
1077
+ * - Browsers (native crypto.randomUUID)
1078
+ * - Node.js 20+ (native crypto.randomUUID)
1079
+ * - React Native with `react-native-get-random-values` polyfill installed
1080
+ */
1081
+ declare function generateUUID(): string;
1082
+ /**
1083
+ * SHA-256 hash a string and return the hex digest.
1084
+ * Used to anonymize distinctId when anonymizeUserId=true.
1085
+ */
1086
+ declare function sha256Hex(input: string): Promise<string>;
1087
+
1088
+ /**
1089
+ * Return the current time as an ISO-8601 UTC string.
1090
+ */
1091
+ declare function nowISO(): string;
1092
+
1093
+ export { type CleanupConfig, type ConsentHistoryEntry, ConsentManager, type ConsentState, type ConsentStatus, type EventContext, type EventCountPeriod, EventCounter, type EventMap, EventQueue, type EventType, FrequencyMiddleware, type FrequencyMiddlewareOptions, type HttpAdapterConfig, type IAnalyticsAdapter, type IEventCounter, type IMiddleware, type IStorageAdapter, type ISunglassesClient, type ISunglassesTypedClient, IdentityManager, type IdentityState, LocalEventArchive, type Logger, type MiddlewareNext, MiddlewarePipeline, PiiSanitizer, SamplingMiddleware, type SamplingMiddlewareOptions, type ScreenTrackingOptions, SessionManager, type SessionState, type StarfishAdapterConfig, type SunglassesConfig, SunglassesCore, type SunglassesEvent, TraitManager, type UserDataExport, asTyped, createLogger, generateUUID, nowISO, sha256Hex };