@drakkar.software/sunglasses-core 0.7.0 → 0.10.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/index.d.mts CHANGED
@@ -11,7 +11,7 @@ interface IStorageAdapter {
11
11
  }
12
12
  /**
13
13
  * Output destination that receives batches of sanitized, consented events.
14
- * Implementations: HttpStorageAdapter, StarfishAnalyticsAdapter, console (debug).
14
+ * Implementations include `HttpStorageAdapter` and any custom adapter.
15
15
  */
16
16
  interface IAnalyticsAdapter {
17
17
  /**
@@ -27,7 +27,7 @@ interface IAnalyticsAdapter {
27
27
  /**
28
28
  * Called after a successful flush with the events that were delivered.
29
29
  * Use this to archive or remove old events from the remote store.
30
- * Implement this in adapters that accumulate data (e.g. StarfishAnalyticsAdapter).
30
+ * Implement in adapters that accumulate data and need post-flush pruning.
31
31
  */
32
32
  cleanupAfterFlush?(delivered: ReadonlyArray<SunglassesEvent>, config: CleanupConfig): Promise<void>;
33
33
  }
@@ -55,6 +55,39 @@ interface IMiddleware {
55
55
  process(event: SunglassesEvent, next: MiddlewareNext): Promise<SunglassesEvent | null>;
56
56
  }
57
57
  type EventType = 'capture' | 'screen' | 'identify' | 'alias' | 'group';
58
+ /**
59
+ * Over-the-air (OTA) / app update information, e.g. an Expo EAS Update or a web
60
+ * deploy identifier. Useful for correlating events with a specific shipped bundle.
61
+ */
62
+ interface AppUpdateInfo {
63
+ /** Unique identifier of the applied update (e.g. EAS update ID). */
64
+ id?: string;
65
+ /** Release channel the update was published to (e.g. 'production'). */
66
+ channel?: string;
67
+ /** Runtime/native version the update is compatible with. */
68
+ runtimeVersion?: string;
69
+ /** True when running the bundle shipped in the binary (no OTA applied). */
70
+ embedded?: boolean;
71
+ /** ISO-8601 timestamp of when the update was created/published. */
72
+ createdAt?: string;
73
+ }
74
+ /**
75
+ * Mutable global metadata attached to every event's `context`. Set via
76
+ * `SunglassesConfig` at init and updatable at runtime. In-memory only — the host
77
+ * app must re-supply these values on each boot.
78
+ */
79
+ interface AppMetadata {
80
+ /** Deployment environment, e.g. 'production' | 'staging' | 'development'. */
81
+ environment?: string;
82
+ /** App variant / build flavor, e.g. 'pro' | 'lite' | 'beta'. */
83
+ appVariant?: string;
84
+ /** OTA / app update info for the currently running bundle. */
85
+ appUpdate?: AppUpdateInfo;
86
+ /** Enabled features / experiment variants (app-scoped). */
87
+ features?: string[];
88
+ /** Active user entitlements (user-scoped, cleared on reset()). */
89
+ entitlements?: string[];
90
+ }
58
91
  interface EventContext {
59
92
  library: {
60
93
  name: string;
@@ -65,6 +98,8 @@ interface EventContext {
65
98
  name?: string;
66
99
  version?: string;
67
100
  build?: string;
101
+ variant?: string;
102
+ update?: AppUpdateInfo;
68
103
  };
69
104
  device?: {
70
105
  type?: string;
@@ -75,6 +110,12 @@ interface EventContext {
75
110
  height?: number;
76
111
  };
77
112
  locale?: string;
113
+ /** Deployment environment, e.g. 'production' | 'staging' | 'development'. */
114
+ environment?: string;
115
+ /** Enabled features / experiment variants (app-scoped). */
116
+ features?: string[];
117
+ /** Active user entitlements (user-scoped). */
118
+ entitlements?: string[];
78
119
  /** Current session ID. Present when enableSessionTracking is true. */
79
120
  sessionId?: string;
80
121
  /** Persisted user traits set via identify(). Forwarded to backends for segmentation. */
@@ -171,6 +212,16 @@ interface SunglassesConfig {
171
212
  appName?: string;
172
213
  appVersion?: string;
173
214
  appBuild?: string;
215
+ /** App variant / build flavor, e.g. 'pro' | 'lite' | 'beta'. Attached to context.app.variant. */
216
+ appVariant?: string;
217
+ /** OTA / app update info for the currently running bundle. Attached to context.app.update. */
218
+ appUpdate?: AppUpdateInfo;
219
+ /** Deployment environment, e.g. 'production' | 'staging'. Attached to context.environment. */
220
+ environment?: string;
221
+ /** Enabled features / experiment variants (app-scoped). Attached to context.features. */
222
+ features?: string[];
223
+ /** Active user entitlements (user-scoped). Attached to context.entitlements. */
224
+ entitlements?: string[];
174
225
  /** Enables verbose console logging. Never enable in production. */
175
226
  debug?: boolean;
176
227
  /** Hard-disables all tracking (e.g. CI environments, test suites). */
@@ -269,6 +320,34 @@ interface ISunglassesClient {
269
320
  unregister(...keys: string[]): void;
270
321
  /** Returns a snapshot of all currently registered super properties. */
271
322
  getRegisteredProperties(): Record<string, unknown>;
323
+ /**
324
+ * Set the deployment environment (e.g. 'production', 'staging'). Attached to
325
+ * every subsequent event's `context.environment`. In-memory only.
326
+ */
327
+ setEnvironment(environment: string): void;
328
+ /**
329
+ * Set OTA / app update info for the currently running bundle. Attached to
330
+ * every subsequent event's `context.app.update`. In-memory only.
331
+ */
332
+ setAppUpdate(update: AppUpdateInfo): void;
333
+ /**
334
+ * Set the list of enabled features / experiment variants (app-scoped).
335
+ * Attached to every subsequent event's `context.features`. In-memory only.
336
+ */
337
+ setFeatures(features: string[]): void;
338
+ /**
339
+ * Set the active user entitlements (user-scoped). Attached to every subsequent
340
+ * event's `context.entitlements`. Cleared by `reset()` and `deleteUserData()`.
341
+ * In-memory only.
342
+ */
343
+ setEntitlements(entitlements: string[]): void;
344
+ /**
345
+ * Merge a partial set of global app metadata. Only the provided keys are
346
+ * updated; omitted keys keep their current value. In-memory only.
347
+ */
348
+ setAppMetadata(meta: Partial<AppMetadata>): void;
349
+ /** Returns a snapshot of the current global app metadata. */
350
+ getAppMetadata(): AppMetadata;
272
351
  optIn(): Promise<void>;
273
352
  optOut(): Promise<void>;
274
353
  hasOptedIn(): boolean;
@@ -366,54 +445,6 @@ interface HttpAdapterConfig {
366
445
  /** Request timeout in ms. Default: 10_000. */
367
446
  timeout?: number;
368
447
  }
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
- * When `true`, events are pushed directly without a prior pull.
404
- * No merge, no optimistic locking, no conflict detection.
405
- *
406
- * Use this for Starfish collections configured with `queueOnly: true` —
407
- * the server ignores `baseHash` and returns no stored data on pull,
408
- * so a pull round-trip is always wasted.
409
- *
410
- * On push failure the adapter **throws**, allowing SunglassesCore to keep
411
- * events in the local queue and retry on the next flush interval.
412
- *
413
- * Cannot be combined with `rotatePathOnSuccess`.
414
- */
415
- pushOnly?: boolean;
416
- }
417
448
  /**
418
449
  * In-memory + persisted session state.
419
450
  * Session IDs are random UUIDs — they never contain PII.
@@ -546,6 +577,12 @@ declare class SunglassesCore implements ISunglassesClient {
546
577
  private readonly superProperties;
547
578
  /** In-memory group ID attached to every event's context after group() is called. */
548
579
  private groupId;
580
+ /** In-memory global app metadata attached to every event's context. */
581
+ private environment;
582
+ private appVariant;
583
+ private appUpdate;
584
+ private features;
585
+ private entitlements;
549
586
  private constructor();
550
587
  /**
551
588
  * Create and initialize a SunglassesCore instance.
@@ -560,6 +597,12 @@ declare class SunglassesCore implements ISunglassesClient {
560
597
  register(properties: Record<string, unknown>): void;
561
598
  unregister(...keys: string[]): void;
562
599
  getRegisteredProperties(): Record<string, unknown>;
600
+ setEnvironment(environment: string): void;
601
+ setAppUpdate(update: AppUpdateInfo): void;
602
+ setFeatures(features: string[]): void;
603
+ setEntitlements(entitlements: string[]): void;
604
+ setAppMetadata(meta: Partial<AppMetadata>): void;
605
+ getAppMetadata(): AppMetadata;
563
606
  reset(): Promise<void>;
564
607
  optIn(): Promise<void>;
565
608
  optOut(): Promise<void>;
@@ -1093,6 +1136,73 @@ declare class LocalEventArchive {
1093
1136
  */
1094
1137
  declare function asTyped<T extends EventMap>(client: ISunglassesClient): ISunglassesTypedClient<T>;
1095
1138
 
1139
+ /**
1140
+ * Options for {@link captureException}. Mirrors the privacy-safe defaults used
1141
+ * by the error-capturing adapters so behaviour is consistent everywhere.
1142
+ */
1143
+ interface CaptureExceptionOptions {
1144
+ /**
1145
+ * Whether the error was handled (caught by an error boundary / try-catch) or
1146
+ * unhandled (surfaced by a global error handler). Default: `true`.
1147
+ */
1148
+ handled?: boolean;
1149
+ /** Sentry-compatible severity level. Default: `'error'`. */
1150
+ level?: string;
1151
+ /**
1152
+ * Include the stack trace in `$error_stack`. Default: `false` (privacy-safe).
1153
+ * Stack traces may expose internal file paths and function names.
1154
+ */
1155
+ includeStack?: boolean;
1156
+ /**
1157
+ * Maximum number of stack frames to include when `includeStack` is `true`.
1158
+ * Default: `5`.
1159
+ */
1160
+ maxStackFrames?: number;
1161
+ /**
1162
+ * Truncate error messages to this many characters. Default: `200`.
1163
+ * Error messages sometimes contain user data ("User foo@bar.com not found").
1164
+ */
1165
+ maxMessageLength?: number;
1166
+ /**
1167
+ * Skip errors whose message matches any of these patterns.
1168
+ * Pattern is tested against the raw (pre-truncation) message.
1169
+ */
1170
+ ignorePatterns?: RegExp[];
1171
+ /**
1172
+ * Extra properties merged into the captured `$error` event. Lower precedence
1173
+ * than the computed `$error_*` properties.
1174
+ */
1175
+ properties?: Record<string, unknown>;
1176
+ /**
1177
+ * Optional transform applied before `client.capture()`.
1178
+ * Receives typed `ErrorEventProperties`; return a (possibly extended) props
1179
+ * object to capture, or `null` to skip capture entirely.
1180
+ */
1181
+ beforeCapture?: (props: ErrorEventProperties) => Record<string, unknown> | null;
1182
+ }
1183
+ /**
1184
+ * Normalize any thrown value into a SunGlasses `$error` event and capture it
1185
+ * via `client.capture()`.
1186
+ *
1187
+ * This is the single source of error-event construction shared by the built-in
1188
+ * error boundaries and the provider global error handlers. It never throws and
1189
+ * respects consent automatically (capture is consent-gated in the core).
1190
+ *
1191
+ * @param client - SunGlasses client instance.
1192
+ * @param error - The thrown value (an `Error`, string, or arbitrary object).
1193
+ * @param options - Optional capture configuration.
1194
+ *
1195
+ * @example
1196
+ * ```ts
1197
+ * try {
1198
+ * doRiskyThing();
1199
+ * } catch (err) {
1200
+ * captureException(client, err); // $error_handled: true
1201
+ * }
1202
+ * ```
1203
+ */
1204
+ declare function captureException(client: ISunglassesClient, error: unknown, options?: CaptureExceptionOptions): void;
1205
+
1096
1206
  /**
1097
1207
  * A typed analytics stub that is safe to use before the SDK initialises.
1098
1208
  *
@@ -1165,4 +1275,4 @@ declare function sha256Hex(input: string): Promise<string>;
1165
1275
  */
1166
1276
  declare function nowISO(): string;
1167
1277
 
1168
- export { type CleanupConfig, type ConsentHistoryEntry, ConsentManager, type ConsentState, type ConsentStatus, type ErrorEventProperties, 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, createLazyClient, createLogger, generateUUID, nowISO, sha256Hex };
1278
+ export { type AppMetadata, type AppUpdateInfo, type CaptureExceptionOptions, type CleanupConfig, type ConsentHistoryEntry, ConsentManager, type ConsentState, type ConsentStatus, type ErrorEventProperties, 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 SunglassesConfig, SunglassesCore, type SunglassesEvent, TraitManager, type UserDataExport, asTyped, captureException, createLazyClient, createLogger, generateUUID, nowISO, sha256Hex };
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ interface IStorageAdapter {
11
11
  }
12
12
  /**
13
13
  * Output destination that receives batches of sanitized, consented events.
14
- * Implementations: HttpStorageAdapter, StarfishAnalyticsAdapter, console (debug).
14
+ * Implementations include `HttpStorageAdapter` and any custom adapter.
15
15
  */
16
16
  interface IAnalyticsAdapter {
17
17
  /**
@@ -27,7 +27,7 @@ interface IAnalyticsAdapter {
27
27
  /**
28
28
  * Called after a successful flush with the events that were delivered.
29
29
  * Use this to archive or remove old events from the remote store.
30
- * Implement this in adapters that accumulate data (e.g. StarfishAnalyticsAdapter).
30
+ * Implement in adapters that accumulate data and need post-flush pruning.
31
31
  */
32
32
  cleanupAfterFlush?(delivered: ReadonlyArray<SunglassesEvent>, config: CleanupConfig): Promise<void>;
33
33
  }
@@ -55,6 +55,39 @@ interface IMiddleware {
55
55
  process(event: SunglassesEvent, next: MiddlewareNext): Promise<SunglassesEvent | null>;
56
56
  }
57
57
  type EventType = 'capture' | 'screen' | 'identify' | 'alias' | 'group';
58
+ /**
59
+ * Over-the-air (OTA) / app update information, e.g. an Expo EAS Update or a web
60
+ * deploy identifier. Useful for correlating events with a specific shipped bundle.
61
+ */
62
+ interface AppUpdateInfo {
63
+ /** Unique identifier of the applied update (e.g. EAS update ID). */
64
+ id?: string;
65
+ /** Release channel the update was published to (e.g. 'production'). */
66
+ channel?: string;
67
+ /** Runtime/native version the update is compatible with. */
68
+ runtimeVersion?: string;
69
+ /** True when running the bundle shipped in the binary (no OTA applied). */
70
+ embedded?: boolean;
71
+ /** ISO-8601 timestamp of when the update was created/published. */
72
+ createdAt?: string;
73
+ }
74
+ /**
75
+ * Mutable global metadata attached to every event's `context`. Set via
76
+ * `SunglassesConfig` at init and updatable at runtime. In-memory only — the host
77
+ * app must re-supply these values on each boot.
78
+ */
79
+ interface AppMetadata {
80
+ /** Deployment environment, e.g. 'production' | 'staging' | 'development'. */
81
+ environment?: string;
82
+ /** App variant / build flavor, e.g. 'pro' | 'lite' | 'beta'. */
83
+ appVariant?: string;
84
+ /** OTA / app update info for the currently running bundle. */
85
+ appUpdate?: AppUpdateInfo;
86
+ /** Enabled features / experiment variants (app-scoped). */
87
+ features?: string[];
88
+ /** Active user entitlements (user-scoped, cleared on reset()). */
89
+ entitlements?: string[];
90
+ }
58
91
  interface EventContext {
59
92
  library: {
60
93
  name: string;
@@ -65,6 +98,8 @@ interface EventContext {
65
98
  name?: string;
66
99
  version?: string;
67
100
  build?: string;
101
+ variant?: string;
102
+ update?: AppUpdateInfo;
68
103
  };
69
104
  device?: {
70
105
  type?: string;
@@ -75,6 +110,12 @@ interface EventContext {
75
110
  height?: number;
76
111
  };
77
112
  locale?: string;
113
+ /** Deployment environment, e.g. 'production' | 'staging' | 'development'. */
114
+ environment?: string;
115
+ /** Enabled features / experiment variants (app-scoped). */
116
+ features?: string[];
117
+ /** Active user entitlements (user-scoped). */
118
+ entitlements?: string[];
78
119
  /** Current session ID. Present when enableSessionTracking is true. */
79
120
  sessionId?: string;
80
121
  /** Persisted user traits set via identify(). Forwarded to backends for segmentation. */
@@ -171,6 +212,16 @@ interface SunglassesConfig {
171
212
  appName?: string;
172
213
  appVersion?: string;
173
214
  appBuild?: string;
215
+ /** App variant / build flavor, e.g. 'pro' | 'lite' | 'beta'. Attached to context.app.variant. */
216
+ appVariant?: string;
217
+ /** OTA / app update info for the currently running bundle. Attached to context.app.update. */
218
+ appUpdate?: AppUpdateInfo;
219
+ /** Deployment environment, e.g. 'production' | 'staging'. Attached to context.environment. */
220
+ environment?: string;
221
+ /** Enabled features / experiment variants (app-scoped). Attached to context.features. */
222
+ features?: string[];
223
+ /** Active user entitlements (user-scoped). Attached to context.entitlements. */
224
+ entitlements?: string[];
174
225
  /** Enables verbose console logging. Never enable in production. */
175
226
  debug?: boolean;
176
227
  /** Hard-disables all tracking (e.g. CI environments, test suites). */
@@ -269,6 +320,34 @@ interface ISunglassesClient {
269
320
  unregister(...keys: string[]): void;
270
321
  /** Returns a snapshot of all currently registered super properties. */
271
322
  getRegisteredProperties(): Record<string, unknown>;
323
+ /**
324
+ * Set the deployment environment (e.g. 'production', 'staging'). Attached to
325
+ * every subsequent event's `context.environment`. In-memory only.
326
+ */
327
+ setEnvironment(environment: string): void;
328
+ /**
329
+ * Set OTA / app update info for the currently running bundle. Attached to
330
+ * every subsequent event's `context.app.update`. In-memory only.
331
+ */
332
+ setAppUpdate(update: AppUpdateInfo): void;
333
+ /**
334
+ * Set the list of enabled features / experiment variants (app-scoped).
335
+ * Attached to every subsequent event's `context.features`. In-memory only.
336
+ */
337
+ setFeatures(features: string[]): void;
338
+ /**
339
+ * Set the active user entitlements (user-scoped). Attached to every subsequent
340
+ * event's `context.entitlements`. Cleared by `reset()` and `deleteUserData()`.
341
+ * In-memory only.
342
+ */
343
+ setEntitlements(entitlements: string[]): void;
344
+ /**
345
+ * Merge a partial set of global app metadata. Only the provided keys are
346
+ * updated; omitted keys keep their current value. In-memory only.
347
+ */
348
+ setAppMetadata(meta: Partial<AppMetadata>): void;
349
+ /** Returns a snapshot of the current global app metadata. */
350
+ getAppMetadata(): AppMetadata;
272
351
  optIn(): Promise<void>;
273
352
  optOut(): Promise<void>;
274
353
  hasOptedIn(): boolean;
@@ -366,54 +445,6 @@ interface HttpAdapterConfig {
366
445
  /** Request timeout in ms. Default: 10_000. */
367
446
  timeout?: number;
368
447
  }
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
- * When `true`, events are pushed directly without a prior pull.
404
- * No merge, no optimistic locking, no conflict detection.
405
- *
406
- * Use this for Starfish collections configured with `queueOnly: true` —
407
- * the server ignores `baseHash` and returns no stored data on pull,
408
- * so a pull round-trip is always wasted.
409
- *
410
- * On push failure the adapter **throws**, allowing SunglassesCore to keep
411
- * events in the local queue and retry on the next flush interval.
412
- *
413
- * Cannot be combined with `rotatePathOnSuccess`.
414
- */
415
- pushOnly?: boolean;
416
- }
417
448
  /**
418
449
  * In-memory + persisted session state.
419
450
  * Session IDs are random UUIDs — they never contain PII.
@@ -546,6 +577,12 @@ declare class SunglassesCore implements ISunglassesClient {
546
577
  private readonly superProperties;
547
578
  /** In-memory group ID attached to every event's context after group() is called. */
548
579
  private groupId;
580
+ /** In-memory global app metadata attached to every event's context. */
581
+ private environment;
582
+ private appVariant;
583
+ private appUpdate;
584
+ private features;
585
+ private entitlements;
549
586
  private constructor();
550
587
  /**
551
588
  * Create and initialize a SunglassesCore instance.
@@ -560,6 +597,12 @@ declare class SunglassesCore implements ISunglassesClient {
560
597
  register(properties: Record<string, unknown>): void;
561
598
  unregister(...keys: string[]): void;
562
599
  getRegisteredProperties(): Record<string, unknown>;
600
+ setEnvironment(environment: string): void;
601
+ setAppUpdate(update: AppUpdateInfo): void;
602
+ setFeatures(features: string[]): void;
603
+ setEntitlements(entitlements: string[]): void;
604
+ setAppMetadata(meta: Partial<AppMetadata>): void;
605
+ getAppMetadata(): AppMetadata;
563
606
  reset(): Promise<void>;
564
607
  optIn(): Promise<void>;
565
608
  optOut(): Promise<void>;
@@ -1093,6 +1136,73 @@ declare class LocalEventArchive {
1093
1136
  */
1094
1137
  declare function asTyped<T extends EventMap>(client: ISunglassesClient): ISunglassesTypedClient<T>;
1095
1138
 
1139
+ /**
1140
+ * Options for {@link captureException}. Mirrors the privacy-safe defaults used
1141
+ * by the error-capturing adapters so behaviour is consistent everywhere.
1142
+ */
1143
+ interface CaptureExceptionOptions {
1144
+ /**
1145
+ * Whether the error was handled (caught by an error boundary / try-catch) or
1146
+ * unhandled (surfaced by a global error handler). Default: `true`.
1147
+ */
1148
+ handled?: boolean;
1149
+ /** Sentry-compatible severity level. Default: `'error'`. */
1150
+ level?: string;
1151
+ /**
1152
+ * Include the stack trace in `$error_stack`. Default: `false` (privacy-safe).
1153
+ * Stack traces may expose internal file paths and function names.
1154
+ */
1155
+ includeStack?: boolean;
1156
+ /**
1157
+ * Maximum number of stack frames to include when `includeStack` is `true`.
1158
+ * Default: `5`.
1159
+ */
1160
+ maxStackFrames?: number;
1161
+ /**
1162
+ * Truncate error messages to this many characters. Default: `200`.
1163
+ * Error messages sometimes contain user data ("User foo@bar.com not found").
1164
+ */
1165
+ maxMessageLength?: number;
1166
+ /**
1167
+ * Skip errors whose message matches any of these patterns.
1168
+ * Pattern is tested against the raw (pre-truncation) message.
1169
+ */
1170
+ ignorePatterns?: RegExp[];
1171
+ /**
1172
+ * Extra properties merged into the captured `$error` event. Lower precedence
1173
+ * than the computed `$error_*` properties.
1174
+ */
1175
+ properties?: Record<string, unknown>;
1176
+ /**
1177
+ * Optional transform applied before `client.capture()`.
1178
+ * Receives typed `ErrorEventProperties`; return a (possibly extended) props
1179
+ * object to capture, or `null` to skip capture entirely.
1180
+ */
1181
+ beforeCapture?: (props: ErrorEventProperties) => Record<string, unknown> | null;
1182
+ }
1183
+ /**
1184
+ * Normalize any thrown value into a SunGlasses `$error` event and capture it
1185
+ * via `client.capture()`.
1186
+ *
1187
+ * This is the single source of error-event construction shared by the built-in
1188
+ * error boundaries and the provider global error handlers. It never throws and
1189
+ * respects consent automatically (capture is consent-gated in the core).
1190
+ *
1191
+ * @param client - SunGlasses client instance.
1192
+ * @param error - The thrown value (an `Error`, string, or arbitrary object).
1193
+ * @param options - Optional capture configuration.
1194
+ *
1195
+ * @example
1196
+ * ```ts
1197
+ * try {
1198
+ * doRiskyThing();
1199
+ * } catch (err) {
1200
+ * captureException(client, err); // $error_handled: true
1201
+ * }
1202
+ * ```
1203
+ */
1204
+ declare function captureException(client: ISunglassesClient, error: unknown, options?: CaptureExceptionOptions): void;
1205
+
1096
1206
  /**
1097
1207
  * A typed analytics stub that is safe to use before the SDK initialises.
1098
1208
  *
@@ -1165,4 +1275,4 @@ declare function sha256Hex(input: string): Promise<string>;
1165
1275
  */
1166
1276
  declare function nowISO(): string;
1167
1277
 
1168
- export { type CleanupConfig, type ConsentHistoryEntry, ConsentManager, type ConsentState, type ConsentStatus, type ErrorEventProperties, 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, createLazyClient, createLogger, generateUUID, nowISO, sha256Hex };
1278
+ export { type AppMetadata, type AppUpdateInfo, type CaptureExceptionOptions, type CleanupConfig, type ConsentHistoryEntry, ConsentManager, type ConsentState, type ConsentStatus, type ErrorEventProperties, 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 SunglassesConfig, SunglassesCore, type SunglassesEvent, TraitManager, type UserDataExport, asTyped, captureException, createLazyClient, createLogger, generateUUID, nowISO, sha256Hex };
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  SunglassesCore: () => SunglassesCore,
34
34
  TraitManager: () => TraitManager,
35
35
  asTyped: () => asTyped,
36
+ captureException: () => captureException,
36
37
  createLazyClient: () => createLazyClient,
37
38
  createLogger: () => createLogger,
38
39
  generateUUID: () => generateUUID,
@@ -1013,6 +1014,11 @@ var SunglassesCore = class _SunglassesCore {
1013
1014
  this.sessionManager = sessionManager;
1014
1015
  this.traitManager = traitManager;
1015
1016
  this.localArchive = localArchive;
1017
+ this.environment = config.environment;
1018
+ this.appVariant = config.appVariant;
1019
+ this.appUpdate = config.appUpdate;
1020
+ this.features = config.features;
1021
+ this.entitlements = config.entitlements;
1016
1022
  }
1017
1023
  /**
1018
1024
  * Create and initialize a SunglassesCore instance.
@@ -1145,11 +1151,41 @@ var SunglassesCore = class _SunglassesCore {
1145
1151
  getRegisteredProperties() {
1146
1152
  return Object.fromEntries(this.superProperties);
1147
1153
  }
1154
+ // ── App metadata ───────────────────────────────────────────────────────────
1155
+ setEnvironment(environment) {
1156
+ this.environment = environment;
1157
+ }
1158
+ setAppUpdate(update) {
1159
+ this.appUpdate = update;
1160
+ }
1161
+ setFeatures(features) {
1162
+ this.features = features;
1163
+ }
1164
+ setEntitlements(entitlements) {
1165
+ this.entitlements = entitlements;
1166
+ }
1167
+ setAppMetadata(meta) {
1168
+ if ("environment" in meta) this.environment = meta.environment;
1169
+ if ("appVariant" in meta) this.appVariant = meta.appVariant;
1170
+ if ("appUpdate" in meta) this.appUpdate = meta.appUpdate;
1171
+ if ("features" in meta) this.features = meta.features;
1172
+ if ("entitlements" in meta) this.entitlements = meta.entitlements;
1173
+ }
1174
+ getAppMetadata() {
1175
+ return {
1176
+ environment: this.environment,
1177
+ appVariant: this.appVariant,
1178
+ appUpdate: this.appUpdate,
1179
+ features: this.features,
1180
+ entitlements: this.entitlements
1181
+ };
1182
+ }
1148
1183
  async reset() {
1149
1184
  await this.identity.reset();
1150
1185
  await this.queue.clear();
1151
1186
  await this.traitManager.clearTraits();
1152
1187
  this.groupId = null;
1188
+ this.entitlements = void 0;
1153
1189
  if (this.sessionManager) {
1154
1190
  await this.sessionManager.end();
1155
1191
  }
@@ -1267,6 +1303,7 @@ var SunglassesCore = class _SunglassesCore {
1267
1303
  await this.identity.reset();
1268
1304
  this.groupId = null;
1269
1305
  this.superProperties.clear();
1306
+ this.entitlements = void 0;
1270
1307
  if (options.resetConsent) {
1271
1308
  await this.consent.resetToUnknown(this.config.consentPolicyVersion);
1272
1309
  this.stopFlushTimer();
@@ -1352,13 +1389,24 @@ var SunglassesCore = class _SunglassesCore {
1352
1389
  library: { name: LIBRARY_NAME, version: LIBRARY_VERSION },
1353
1390
  platform: this.config.platform
1354
1391
  };
1355
- if (this.config.appName || this.config.appVersion) {
1392
+ if (this.config.appName || this.config.appVersion || this.appVariant || this.appUpdate) {
1356
1393
  ctx.app = {
1357
1394
  name: this.config.appName || void 0,
1358
1395
  version: this.config.appVersion || void 0,
1359
- build: this.config.appBuild || void 0
1396
+ build: this.config.appBuild || void 0,
1397
+ variant: this.appVariant || void 0,
1398
+ update: this.appUpdate || void 0
1360
1399
  };
1361
1400
  }
1401
+ if (this.environment) {
1402
+ ctx.environment = this.environment;
1403
+ }
1404
+ if (this.features && this.features.length > 0) {
1405
+ ctx.features = this.features;
1406
+ }
1407
+ if (this.entitlements && this.entitlements.length > 0) {
1408
+ ctx.entitlements = this.entitlements;
1409
+ }
1362
1410
  if (sessionId !== void 0) {
1363
1411
  ctx.sessionId = sessionId;
1364
1412
  }
@@ -1532,6 +1580,71 @@ function asTyped(client) {
1532
1580
  return client;
1533
1581
  }
1534
1582
 
1583
+ // src/captureException.ts
1584
+ function normalizeError(error) {
1585
+ if (error instanceof Error) {
1586
+ return { message: error.message, name: error.name || "Error", stack: error.stack };
1587
+ }
1588
+ if (typeof error === "string") {
1589
+ return { message: error, name: "Error" };
1590
+ }
1591
+ if (error && typeof error === "object") {
1592
+ const maybe = error;
1593
+ return {
1594
+ message: typeof maybe.message === "string" ? maybe.message : String(error),
1595
+ name: typeof maybe.name === "string" && maybe.name ? maybe.name : "Error",
1596
+ stack: typeof maybe.stack === "string" ? maybe.stack : void 0
1597
+ };
1598
+ }
1599
+ return { message: String(error), name: "Error" };
1600
+ }
1601
+ function extractStack(stack, maxFrames) {
1602
+ if (!stack) return void 0;
1603
+ const lines = stack.split("\n").map((l) => l.trim());
1604
+ const v8Frames = lines.filter((l) => l.startsWith("at "));
1605
+ if (v8Frames.length > 0) {
1606
+ return v8Frames.slice(0, maxFrames).join("\n");
1607
+ }
1608
+ const rnFrames = lines.filter((l) => l.includes("@") && !l.includes(" "));
1609
+ if (rnFrames.length > 0) {
1610
+ return rnFrames.slice(0, maxFrames).join("\n");
1611
+ }
1612
+ return void 0;
1613
+ }
1614
+ function captureException(client, error, options = {}) {
1615
+ const {
1616
+ handled = true,
1617
+ level = "error",
1618
+ includeStack = false,
1619
+ maxStackFrames = 5,
1620
+ maxMessageLength = 200,
1621
+ ignorePatterns = [],
1622
+ properties,
1623
+ beforeCapture
1624
+ } = options;
1625
+ const normalized = normalizeError(error);
1626
+ const rawMessage = normalized.message;
1627
+ if (ignorePatterns.some((p) => p.test(rawMessage))) return;
1628
+ let props = {
1629
+ ...properties,
1630
+ $error_message: rawMessage.slice(0, maxMessageLength),
1631
+ $error_type: normalized.name,
1632
+ $error_handled: handled,
1633
+ $error_level: level
1634
+ };
1635
+ if (includeStack) {
1636
+ const frames = extractStack(normalized.stack, maxStackFrames);
1637
+ if (frames) props = { ...props, $error_stack: frames };
1638
+ }
1639
+ if (beforeCapture) {
1640
+ const transformed = beforeCapture(props);
1641
+ if (!transformed) return;
1642
+ client.capture("$error", transformed);
1643
+ } else {
1644
+ client.capture("$error", props);
1645
+ }
1646
+ }
1647
+
1535
1648
  // src/LazyClient.ts
1536
1649
  function createLazyClient() {
1537
1650
  let _inner = null;
@@ -1568,6 +1681,25 @@ function createLazyClient() {
1568
1681
  getRegisteredProperties() {
1569
1682
  return _inner?.getRegisteredProperties() ?? {};
1570
1683
  },
1684
+ // ── App metadata ──────────────────────────────────────────────────────────
1685
+ setEnvironment(environment) {
1686
+ _inner?.setEnvironment(environment);
1687
+ },
1688
+ setAppUpdate(update) {
1689
+ _inner?.setAppUpdate(update);
1690
+ },
1691
+ setFeatures(features) {
1692
+ _inner?.setFeatures(features);
1693
+ },
1694
+ setEntitlements(entitlements) {
1695
+ _inner?.setEntitlements(entitlements);
1696
+ },
1697
+ setAppMetadata(meta) {
1698
+ _inner?.setAppMetadata(meta);
1699
+ },
1700
+ getAppMetadata() {
1701
+ return _inner?.getAppMetadata() ?? {};
1702
+ },
1571
1703
  // ── Consent ───────────────────────────────────────────────────────────────
1572
1704
  async optIn() {
1573
1705
  await _inner?.optIn();
@@ -1645,6 +1777,7 @@ function createLazyClient() {
1645
1777
  SunglassesCore,
1646
1778
  TraitManager,
1647
1779
  asTyped,
1780
+ captureException,
1648
1781
  createLazyClient,
1649
1782
  createLogger,
1650
1783
  generateUUID,
package/dist/index.mjs CHANGED
@@ -970,6 +970,11 @@ var SunglassesCore = class _SunglassesCore {
970
970
  this.sessionManager = sessionManager;
971
971
  this.traitManager = traitManager;
972
972
  this.localArchive = localArchive;
973
+ this.environment = config.environment;
974
+ this.appVariant = config.appVariant;
975
+ this.appUpdate = config.appUpdate;
976
+ this.features = config.features;
977
+ this.entitlements = config.entitlements;
973
978
  }
974
979
  /**
975
980
  * Create and initialize a SunglassesCore instance.
@@ -1102,11 +1107,41 @@ var SunglassesCore = class _SunglassesCore {
1102
1107
  getRegisteredProperties() {
1103
1108
  return Object.fromEntries(this.superProperties);
1104
1109
  }
1110
+ // ── App metadata ───────────────────────────────────────────────────────────
1111
+ setEnvironment(environment) {
1112
+ this.environment = environment;
1113
+ }
1114
+ setAppUpdate(update) {
1115
+ this.appUpdate = update;
1116
+ }
1117
+ setFeatures(features) {
1118
+ this.features = features;
1119
+ }
1120
+ setEntitlements(entitlements) {
1121
+ this.entitlements = entitlements;
1122
+ }
1123
+ setAppMetadata(meta) {
1124
+ if ("environment" in meta) this.environment = meta.environment;
1125
+ if ("appVariant" in meta) this.appVariant = meta.appVariant;
1126
+ if ("appUpdate" in meta) this.appUpdate = meta.appUpdate;
1127
+ if ("features" in meta) this.features = meta.features;
1128
+ if ("entitlements" in meta) this.entitlements = meta.entitlements;
1129
+ }
1130
+ getAppMetadata() {
1131
+ return {
1132
+ environment: this.environment,
1133
+ appVariant: this.appVariant,
1134
+ appUpdate: this.appUpdate,
1135
+ features: this.features,
1136
+ entitlements: this.entitlements
1137
+ };
1138
+ }
1105
1139
  async reset() {
1106
1140
  await this.identity.reset();
1107
1141
  await this.queue.clear();
1108
1142
  await this.traitManager.clearTraits();
1109
1143
  this.groupId = null;
1144
+ this.entitlements = void 0;
1110
1145
  if (this.sessionManager) {
1111
1146
  await this.sessionManager.end();
1112
1147
  }
@@ -1224,6 +1259,7 @@ var SunglassesCore = class _SunglassesCore {
1224
1259
  await this.identity.reset();
1225
1260
  this.groupId = null;
1226
1261
  this.superProperties.clear();
1262
+ this.entitlements = void 0;
1227
1263
  if (options.resetConsent) {
1228
1264
  await this.consent.resetToUnknown(this.config.consentPolicyVersion);
1229
1265
  this.stopFlushTimer();
@@ -1309,13 +1345,24 @@ var SunglassesCore = class _SunglassesCore {
1309
1345
  library: { name: LIBRARY_NAME, version: LIBRARY_VERSION },
1310
1346
  platform: this.config.platform
1311
1347
  };
1312
- if (this.config.appName || this.config.appVersion) {
1348
+ if (this.config.appName || this.config.appVersion || this.appVariant || this.appUpdate) {
1313
1349
  ctx.app = {
1314
1350
  name: this.config.appName || void 0,
1315
1351
  version: this.config.appVersion || void 0,
1316
- build: this.config.appBuild || void 0
1352
+ build: this.config.appBuild || void 0,
1353
+ variant: this.appVariant || void 0,
1354
+ update: this.appUpdate || void 0
1317
1355
  };
1318
1356
  }
1357
+ if (this.environment) {
1358
+ ctx.environment = this.environment;
1359
+ }
1360
+ if (this.features && this.features.length > 0) {
1361
+ ctx.features = this.features;
1362
+ }
1363
+ if (this.entitlements && this.entitlements.length > 0) {
1364
+ ctx.entitlements = this.entitlements;
1365
+ }
1319
1366
  if (sessionId !== void 0) {
1320
1367
  ctx.sessionId = sessionId;
1321
1368
  }
@@ -1489,6 +1536,71 @@ function asTyped(client) {
1489
1536
  return client;
1490
1537
  }
1491
1538
 
1539
+ // src/captureException.ts
1540
+ function normalizeError(error) {
1541
+ if (error instanceof Error) {
1542
+ return { message: error.message, name: error.name || "Error", stack: error.stack };
1543
+ }
1544
+ if (typeof error === "string") {
1545
+ return { message: error, name: "Error" };
1546
+ }
1547
+ if (error && typeof error === "object") {
1548
+ const maybe = error;
1549
+ return {
1550
+ message: typeof maybe.message === "string" ? maybe.message : String(error),
1551
+ name: typeof maybe.name === "string" && maybe.name ? maybe.name : "Error",
1552
+ stack: typeof maybe.stack === "string" ? maybe.stack : void 0
1553
+ };
1554
+ }
1555
+ return { message: String(error), name: "Error" };
1556
+ }
1557
+ function extractStack(stack, maxFrames) {
1558
+ if (!stack) return void 0;
1559
+ const lines = stack.split("\n").map((l) => l.trim());
1560
+ const v8Frames = lines.filter((l) => l.startsWith("at "));
1561
+ if (v8Frames.length > 0) {
1562
+ return v8Frames.slice(0, maxFrames).join("\n");
1563
+ }
1564
+ const rnFrames = lines.filter((l) => l.includes("@") && !l.includes(" "));
1565
+ if (rnFrames.length > 0) {
1566
+ return rnFrames.slice(0, maxFrames).join("\n");
1567
+ }
1568
+ return void 0;
1569
+ }
1570
+ function captureException(client, error, options = {}) {
1571
+ const {
1572
+ handled = true,
1573
+ level = "error",
1574
+ includeStack = false,
1575
+ maxStackFrames = 5,
1576
+ maxMessageLength = 200,
1577
+ ignorePatterns = [],
1578
+ properties,
1579
+ beforeCapture
1580
+ } = options;
1581
+ const normalized = normalizeError(error);
1582
+ const rawMessage = normalized.message;
1583
+ if (ignorePatterns.some((p) => p.test(rawMessage))) return;
1584
+ let props = {
1585
+ ...properties,
1586
+ $error_message: rawMessage.slice(0, maxMessageLength),
1587
+ $error_type: normalized.name,
1588
+ $error_handled: handled,
1589
+ $error_level: level
1590
+ };
1591
+ if (includeStack) {
1592
+ const frames = extractStack(normalized.stack, maxStackFrames);
1593
+ if (frames) props = { ...props, $error_stack: frames };
1594
+ }
1595
+ if (beforeCapture) {
1596
+ const transformed = beforeCapture(props);
1597
+ if (!transformed) return;
1598
+ client.capture("$error", transformed);
1599
+ } else {
1600
+ client.capture("$error", props);
1601
+ }
1602
+ }
1603
+
1492
1604
  // src/LazyClient.ts
1493
1605
  function createLazyClient() {
1494
1606
  let _inner = null;
@@ -1525,6 +1637,25 @@ function createLazyClient() {
1525
1637
  getRegisteredProperties() {
1526
1638
  return _inner?.getRegisteredProperties() ?? {};
1527
1639
  },
1640
+ // ── App metadata ──────────────────────────────────────────────────────────
1641
+ setEnvironment(environment) {
1642
+ _inner?.setEnvironment(environment);
1643
+ },
1644
+ setAppUpdate(update) {
1645
+ _inner?.setAppUpdate(update);
1646
+ },
1647
+ setFeatures(features) {
1648
+ _inner?.setFeatures(features);
1649
+ },
1650
+ setEntitlements(entitlements) {
1651
+ _inner?.setEntitlements(entitlements);
1652
+ },
1653
+ setAppMetadata(meta) {
1654
+ _inner?.setAppMetadata(meta);
1655
+ },
1656
+ getAppMetadata() {
1657
+ return _inner?.getAppMetadata() ?? {};
1658
+ },
1528
1659
  // ── Consent ───────────────────────────────────────────────────────────────
1529
1660
  async optIn() {
1530
1661
  await _inner?.optIn();
@@ -1601,6 +1732,7 @@ export {
1601
1732
  SunglassesCore,
1602
1733
  TraitManager,
1603
1734
  asTyped,
1735
+ captureException,
1604
1736
  createLazyClient,
1605
1737
  createLogger,
1606
1738
  generateUUID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakkar.software/sunglasses-core",
3
- "version": "0.7.0",
3
+ "version": "0.10.0",
4
4
  "description": "Platform-agnostic event tracking engine for SunGlasses",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",