@emit-vision/sdk-js 0.4.3 → 0.5.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/CHANGELOG.md ADDED
@@ -0,0 +1,76 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@emit-vision/sdk-js` are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ This file starts at 0.4.3 — earlier releases aren't reconstructed here; see the
7
+ [platform changelog](https://emit.vision/docs/changelog) for the full history.
8
+
9
+ ## [Unreleased]
10
+
11
+ _Nothing yet._
12
+
13
+ ## [0.5.0] - 2026-11-11
14
+
15
+ ### Added
16
+
17
+ - Consent API: `grantConsent()`, `declineConsent()`, `getConsentState()` and
18
+ `onConsentChange()`, with a `"unknown" | "granted" | "declined"` state model.
19
+ The choice is remembered for 12 months (override storage with
20
+ `consentStorage`), the browser's Global Privacy Control signal counts as a
21
+ decline unless `honorGlobalPrivacyControl: false`, and each choice sends one
22
+ anonymous `/v1/consent` count with no visitor identifier. `declineConsent()`
23
+ discards queued events. `consent()` still works but is deprecated.
24
+ - UTM parameters (allowlisted `utm_*` keys) are captured on page views and
25
+ attached, with `referrer`, to every browser event.
26
+ - `$page_view` events carry a `referrer` property: the raw, unparsed value of
27
+ `document.referrer`. An empty string means direct navigation, which is
28
+ distinct from an older SDK that omits the field. Classification happens
29
+ server-side.
30
+
31
+ ### Changed
32
+
33
+ - Auto-captured URLs (error `context.url`, default `feedback.url`, page-view and
34
+ attribution `referrer`) now drop the query string and hash. This is a
35
+ behaviour change for anyone relying on query strings in error context; opt
36
+ keys back in with the new `urlQueryAllowlist` option.
37
+
38
+ ### Fixed
39
+
40
+ - The `sdk.version` reported in every envelope now matches the package
41
+ version. The constant had drifted behind `package.json`, so events were
42
+ attributed to an older release. A test now fails if the two disagree.
43
+ - One over-limit string no longer poisons the batch. Events with an empty or
44
+ over-200-character `name` are dropped (an `sdk.event_truncated` meta-event
45
+ with `dropped: true` is queued instead), and over-limit `environment`,
46
+ `release`, `sessionId` and `groupId` are truncated on a code-point boundary
47
+ and listed in the meta-event's `trimmedFields`. Previously `/v1/batch`
48
+ rejected the whole batch on every retry until the SDK disabled itself.
49
+ - Oversized events no longer poison the queue. After trimming `properties`,
50
+ `context` and `error.stack`, an error whose `error.message` is still too
51
+ large is truncated (code-point safe, `…[truncated]` suffix); anything still
52
+ over 64 KiB (huge `name`, `tags`, …) is dropped and only an
53
+ `sdk.event_truncated` meta-event with `dropped: true` is queued. Empty
54
+ `properties`/`context` are no longer reported as trimmed, and the
55
+ meta-event's `originalName` is clamped to 200 characters.
56
+
57
+ ## [0.4.3] - 2026-08-06
58
+
59
+ ### Fixed
60
+
61
+ - `getFlag()` now scopes its request to the requested key instead of
62
+ fetching every flag and indexing locally. This matters because the API's
63
+ `POST /v1/flags/evaluate` route now rejects unscoped requests from
64
+ ingest-scope keys with `400 flag_keys_required` — SDKs on earlier versions
65
+ silently fall back to the caller's default value with no error. Upgrade to
66
+ pick this fix up, or pass `flagKeys` explicitly on every `evaluateFlags()`
67
+ call.
68
+ - The flag-eval cache key now includes the requested key set. Previously a
69
+ cached response for one key set could satisfy a lookup for a different key
70
+ set and silently resolve to the wrong (fallback) value.
71
+
72
+ ### Notes
73
+
74
+ - Pairs with a server-side change: `POST /v1/flags/evaluate` requires
75
+ `flagKeys` from ingest-scope keys (the key type that ships in browser
76
+ bundles). Full-scope API keys and dashboard sessions are unaffected.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Browser SDK for Emit Vision, the self-hostable analytics and error tracking stack in this repo.
4
4
 
5
- For a full local walkthrough, see [docs/sdk-guide.md](../../docs/sdk-guide.md).
5
+ 📖 **[Full sdk-js reference →](https://emitvision.com/docs/sdk-js)**
6
6
 
7
7
  ## Installation
8
8
 
@@ -21,7 +21,7 @@ pnpm pack
21
21
  Then install the generated tarball:
22
22
 
23
23
  ```bash
24
- npm install /absolute/path/to/emit-vision/packages/sdk-js/emit-vision-sdk-js-0.1.0.tgz
24
+ npm install /absolute/path/to/emit-vision/packages/sdk-js/emit-vision-sdk-js-0.5.0.tgz
25
25
  ```
26
26
 
27
27
  ## Quick Start
@@ -70,22 +70,32 @@ browser-only privacy and safety guidance still applies.
70
70
 
71
71
  Initializes the SDK. Call it once, as early as possible.
72
72
 
73
- | Option | Type | Default | Description |
74
- | ------------------- | ----------------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------- | ------ | --- | ---------------------------------------------------------- |
75
- | `dsn` | `string` | — | Local DSN in the form `http://<api-key>@localhost:4301/v1` |
76
- | `apiKey` | `string` | — | Explicit API key if you do not want to use a DSN |
77
- | `endpoint` | `string` | `http://localhost:4301` | Explicit ingest API base URL |
78
- | `environment` | `string` | — | Environment label such as `development` or `production` |
79
- | `release` | `string` | — | Release or git SHA attached to outgoing events |
80
- | `sessionId` | `string` | — | Session identifier attached to future events |
81
- | `deployment` | `{ deploymentId?: string; buildId?: string }` | — | Optional deployment metadata attached to outgoing events |
82
- | `featureFlags` | `Record<string, string | number | boolean | null>` | — | Optional feature-flag snapshot attached to outgoing events |
83
- | `autoCapture` | `{ errors?: boolean; unhandledRejections?: boolean }` | `{ errors: true, unhandledRejections: true }` | Controls automatic browser error capture |
84
- | `autoCaptureErrors` | `boolean` | `true` | Backward-compatible alias that enables or disables both auto-capture hooks |
85
- | `debug` | `boolean` | `false` | Emits SDK lifecycle messages to `console.debug` |
86
- | `flushIntervalMs` | `number` | `5000` | Automatic flush cadence in milliseconds |
87
- | `batchSize` | `number` | `20` | Queue size that triggers an immediate flush |
88
- | `flagEvalTtlMs` | `number` | `60000` | Default TTL (ms) for the in-memory flag evaluation cache |
73
+ | Option | Type | Default | Description |
74
+ | ----------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
75
+ | `dsn` | `string` | — | Local DSN in the form `http://<api-key>@localhost:4301/v1` |
76
+ | `apiKey` | `string` | — | Explicit API key if you do not want to use a DSN |
77
+ | `endpoint` | `string` | `http://localhost:4301` | Explicit ingest API base URL |
78
+ | `environment` | `string` | — | Environment label such as `development` or `production` |
79
+ | `release` | `string` | — | Release or git SHA attached to outgoing events |
80
+ | `sessionId` | `string` | — | Session identifier attached to future events |
81
+ | `label` | `string` | — | Optional human-readable label attached to outgoing events |
82
+ | `deployment` | `{ deploymentId?: string; buildId?: string }` | — | Optional deployment metadata attached to outgoing events |
83
+ | `featureFlags` | `Record<string, string \| number \| boolean \| null>` | — | Optional feature-flag snapshot attached to outgoing events |
84
+ | `projectId` | `string` | — | Passed through to the DevTools debug channel so the extension can build dashboard deep links. Not required for event capture. |
85
+ | `groupId` | `string` | — | Initial group ID to associate with all events. Can be overridden per-event or updated via `group()`. |
86
+ | `autoCapture` | `{ errors?: boolean; unhandledRejections?: boolean; flagExposures?: boolean; pageViews?: boolean }` | `{ errors: true, unhandledRejections: true, flagExposures: true, pageViews: false }` | Controls automatic error capture, `$flag_exposure` events, and `$page_view` events on navigation |
87
+ | `autoCaptureErrors` | `boolean` | `true` | Backward-compatible alias that enables or disables the `errors`/`unhandledRejections` auto-capture hooks |
88
+ | `debug` | `boolean` | `false` | Emits SDK lifecycle messages to `console.debug` |
89
+ | `flushOnCapture` | `boolean` | `false` | Flush immediately after every capture instead of waiting for the batch size or interval |
90
+ | `flushIntervalMs` | `number` | `5000` | Automatic flush cadence in milliseconds |
91
+ | `batchSize` | `number` | `20` | Queue size that triggers an immediate flush |
92
+ | `fetchImpl` | `typeof fetch` | `globalThis.fetch` | Override the `fetch` implementation used to send events (e.g. in non-browser test environments) |
93
+ | `flagEvalTtlMs` | `number` | `60000` | Default TTL (ms) for the in-memory flag evaluation cache |
94
+ | `retryBackoffInitialMs` | `number` | `1000` | Initial delay (ms) before retrying after a flush failure. Doubles each consecutive failure up to `retryBackoffMaxMs`. |
95
+ | `retryBackoffMaxMs` | `number` | `30000` | Maximum delay (ms) between flush retries |
96
+ | `maxConsecutiveFlushFailures` | `number` | `20` | Disable the SDK after this many consecutive flush failures: the queue is dropped and future captures become no-ops. `0` retries forever. |
97
+ | `consentRequired` | `boolean` | `false` | When true, events are queued in memory but not sent until `grantConsent()` is called |
98
+ | `consentGiven` | `boolean` | `false` | Set to true when `consentRequired` is true but consent is already known at init time (e.g. read from a cookie server-side). Skips the consent gate. |
89
99
 
90
100
  ### `captureEvent(name: string, properties?, options?): void`
91
101
 
@@ -101,6 +111,40 @@ captureError(new Error("Payment failed"), {
101
111
  });
102
112
  ```
103
113
 
114
+ ### `capturePageView(path?: string): void`
115
+
116
+ Fires a `$page_view` event. Called automatically on init and on browser navigation when
117
+ `autoCapture.pageViews` is `true`; call it manually for finer control.
118
+
119
+ ```ts
120
+ capturePageView("/checkout/confirmation");
121
+ ```
122
+
123
+ The event always includes `page` (`path`, or `window.location.pathname` when omitted) and
124
+ `referrer` (`document.referrer` without its query string or hash, or `""` for direct/bookmark navigation — sent as an empty
125
+ string rather than omitted so the server can distinguish "direct" from "SDK too old to send
126
+ it"). `document.referrer` reflects the _document's_ referrer and does not update on
127
+ `pushState`/`replaceState`, so later SPA navigations keep reporting the session's original
128
+ external referrer rather than the previous in-app page — that's deliberate, since attribution
129
+ wants "where did this session come from."
130
+
131
+ Any `utm_source`, `utm_medium`, `utm_campaign`, or `utm_content` query parameters present on
132
+ the current URL are also attached (`utm_term` is intentionally never read). Only params
133
+ actually present in the query string are included — an absent param is omitted, not sent as
134
+ `""`, preserving the same absent-vs-empty signal as `referrer`.
135
+
136
+ ### `captureFeedback(feedback, options?): void`
137
+
138
+ ```ts
139
+ captureFeedback({
140
+ message: "Checkout felt slow today",
141
+ rating: 4,
142
+ });
143
+ ```
144
+
145
+ `feedback.url` defaults to the current page's origin + pathname (no query string or hash) when omitted; an explicit `url` is sent as given. `rating` and `url` are left
146
+ off the event entirely when not available, rather than sent as `undefined`.
147
+
104
148
  ### `identify(userId: string, traits?): void`
105
149
 
106
150
  ### `identify(user: EmitVisionUser): void`
@@ -136,12 +180,44 @@ setFeatureFlags({ newCheckout: true, pricingVariant: "control" });
136
180
  setTags({ team: "payments", feature: "checkout" });
137
181
  ```
138
182
 
183
+ ### `group(groupId: string, traits?: Record<string, unknown>): void`
184
+
185
+ Associates subsequent events with a group (organization, team, workspace) rather than an
186
+ individual user.
187
+
188
+ ```ts
189
+ group("org_456", { plan: "enterprise", seats: 42 });
190
+ ```
191
+
139
192
  ### `flush(): Promise<void>`
140
193
 
141
194
  ```ts
142
195
  await flush();
143
196
  ```
144
197
 
198
+ ### Consent: `grantConsent()`, `declineConsent()`, `getConsentState()`, `onConsentChange()`
199
+
200
+ With `consentRequired: true`, events are held in memory until the user decides.
201
+
202
+ ```ts
203
+ init({ dsn: "...", consentRequired: true });
204
+
205
+ captureEvent("page_loaded"); // held, nothing sent
206
+
207
+ await grantConsent(); // held events are flushed
208
+ declineConsent(); // queue dropped, persisted queue cleared, capture is a no-op
209
+
210
+ getConsentState(); // "unknown" | "granted" | "declined"
211
+ const off = onConsentChange((state) => {
212
+ /* gate your other tools */
213
+ });
214
+ ```
215
+
216
+ - The choice is remembered in `localStorage` for 12 months, then the state returns to `"unknown"`.
217
+ - If the browser sends Global Privacy Control and no choice is stored, the state starts as `"declined"` (turn off with `honorGlobalPrivacyControl: false`). An explicit `grantConsent()` overrides it.
218
+ - Each change is reported once to `POST /v1/consent` as an anonymous count (`{ choice, source }`, nothing else).
219
+ - `consent()` still works as a deprecated alias of `grantConsent()`.
220
+
145
221
  ## Feature Flag Evaluation
146
222
 
147
223
  The SDK can fetch and evaluate feature flags from your Emit Vision project. Evaluated
@@ -191,6 +267,17 @@ the network is unavailable. Never throws.
191
267
  Bypasses the in-memory cache and performs a fresh fetch. Useful immediately after a
192
268
  flag change in the dashboard.
193
269
 
270
+ ### `captureExposure(flagKey, variantKey, variantValue?, reason?, environment?): void`
271
+
272
+ Fires a `$flag_exposure` event recording which flag variant a user actually saw. Sent
273
+ automatically after each `evaluateFlags()`/`getFlag()`/`refreshFlags()` call when
274
+ `autoCapture.flagExposures` is `true` (the default); call it manually if you evaluate
275
+ flags outside the SDK's flag-evaluation path.
276
+
277
+ ```ts
278
+ captureExposure("new-checkout", "control", false, "default fallback");
279
+ ```
280
+
194
281
  ### `FlagEvaluationOptions`
195
282
 
196
283
  | Option | Type | Required | Description |
@@ -219,6 +306,24 @@ init({
219
306
  - Use the helper packages when they reduce boilerplate, but keep direct browser
220
307
  SDK usage for the simplest client-side apps.
221
308
 
309
+ ## Documentation
310
+
311
+ - [sdk-js reference](https://emitvision.com/docs/sdk-js) — full API reference and guides
312
+ - [Getting started](https://emitvision.com/docs/introduction)
313
+ - [Installation & configuration](https://emitvision.com/docs/installation)
314
+ - [React SDK](https://emitvision.com/docs/sdk-react) · [Next.js SDK](https://emitvision.com/docs/sdk-next) · [Node SDK](https://emitvision.com/docs/sdk-node)
315
+ - [Troubleshooting](https://emitvision.com/docs/troubleshooting)
316
+
222
317
  ## License
223
318
 
224
319
  MIT — see [LICENSE](./LICENSE).
320
+
321
+ ## Privacy: URLs
322
+
323
+ URLs the SDK captures on its own (the `url` on auto-captured errors, the default `feedback.url`, and the `referrer` on page views and attribution) are reduced to origin + pathname. Query strings and hashes often carry tokens, codes and emails, so they are dropped. To keep specific query keys, list them:
324
+
325
+ ```ts
326
+ init({ apiKey, urlQueryAllowlist: ["tab", "page"] });
327
+ ```
328
+
329
+ URLs you pass explicitly (for example `feedback.url`) are sent untouched. `utm_*` parameters are still read from the page URL as before.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,14 @@
1
+ type ConsentState = "unknown" | "granted" | "declined";
2
+ type ConsentChoiceOptions = {
3
+ source?: "api" | "banner";
4
+ };
5
+ type ConsentStorage = {
6
+ getItem(key: string): string | null;
7
+ setItem(key: string, value: string): void | Promise<void>;
8
+ removeItem(key: string): void | Promise<void>;
9
+ };
10
+ declare function consentStorageKey(apiKey: string, projectId?: string): string;
11
+
1
12
  type EmitVisionUser = {
2
13
  id?: string;
3
14
  email?: string;
@@ -73,12 +84,18 @@ type EmitVisionOptions = {
73
84
  retryBackoffMaxMs?: number;
74
85
  /** Disable the SDK after this many consecutive flush failures: the queue is dropped, future captures become no-ops, and a single warning is logged. Default: 20. Set to 0 to disable the cap and retry forever. */
75
86
  maxConsecutiveFlushFailures?: number;
76
- /** When true, events are queued in memory but not sent until `consent()` is called. Default: false. */
87
+ /** When true, events are queued in memory but not sent until `grantConsent()` is called; the choice is remembered for 12 months. Default: false. */
77
88
  consentRequired?: boolean;
78
89
  /** Set to true when `consentRequired` is true but consent is already known at init time (e.g. read from a cookie server-side). Skips the consent gate. Default: false. */
79
90
  consentGiven?: boolean;
91
+ /** When `consentRequired` is true and no choice is stored, treat the browser's Global Privacy Control signal as a decline. An explicit `grantConsent()` later overrides it. Default: true. */
92
+ honorGlobalPrivacyControl?: boolean;
93
+ /** Where the consent choice is persisted when `localStorage` is unavailable (e.g. React Native). `getItem` must answer synchronously; writes may be async. Default: `localStorage`. */
94
+ consentStorage?: ConsentStorage;
95
+ /** Query-string keys to keep on auto-captured URLs. Default: none, so query and hash are stripped. */
96
+ urlQueryAllowlist?: string[];
80
97
  };
81
- type RequiredEmitVisionOptions = Required<Pick<EmitVisionOptions, "apiKey" | "endpoint" | "flushIntervalMs" | "batchSize" | "fetchImpl" | "flushOnCapture" | "flagEvalTtlMs" | "retryBackoffInitialMs" | "retryBackoffMaxMs" | "maxConsecutiveFlushFailures">> & Pick<EmitVisionOptions, "environment" | "release" | "sessionId" | "label" | "deployment" | "featureFlags" | "debug"> & {
98
+ type RequiredEmitVisionOptions = Required<Pick<EmitVisionOptions, "apiKey" | "endpoint" | "flushIntervalMs" | "batchSize" | "fetchImpl" | "flushOnCapture" | "flagEvalTtlMs" | "retryBackoffInitialMs" | "retryBackoffMaxMs" | "maxConsecutiveFlushFailures" | "urlQueryAllowlist">> & Pick<EmitVisionOptions, "environment" | "release" | "sessionId" | "label" | "deployment" | "featureFlags" | "debug"> & {
82
99
  autoCapture: Required<AutoCaptureOptions> & {
83
100
  flagExposures: boolean;
84
101
  pageViews: boolean;
@@ -87,6 +104,7 @@ type RequiredEmitVisionOptions = Required<Pick<EmitVisionOptions, "apiKey" | "en
87
104
  groupId?: string;
88
105
  consentRequired?: boolean;
89
106
  consentGiven?: boolean;
107
+ consentStorage?: ConsentStorage;
90
108
  };
91
109
 
92
110
  type DevToolsMessage = {
@@ -143,11 +161,12 @@ declare class EmitVisionClient {
143
161
  private deployment;
144
162
  private featureFlags;
145
163
  private tags;
146
- private readonly flagEval;
164
+ private readonly flags;
147
165
  private readonly autoCapture;
148
166
  private readonly flusher;
149
- private readonly persistOnHide;
150
- private readonly persistOnHidden;
167
+ private readonly consentApi;
168
+ private readonly stopPersistOnHide;
169
+ private readonly attribution;
151
170
  constructor(options: RequiredEmitVisionOptions);
152
171
  captureEvent(name: string, properties?: Record<string, unknown>, options?: CaptureOptions): void;
153
172
  captureError(error: unknown, options?: CaptureOptions): void;
@@ -165,8 +184,13 @@ declare class EmitVisionClient {
165
184
  rating?: number;
166
185
  url?: string;
167
186
  }, options?: CaptureOptions): void;
168
- captureExposure(flagKey: string, variantKey: string, variantValue?: string | number | boolean | null, reason?: string, environment?: string): void;
187
+ captureExposure(flagKey: string, variantKey: string, variantValue?: string | number | boolean | null, reason?: string, environment?: string | undefined): void;
169
188
  setTags(tags: Record<string, string>): void;
189
+ grantConsent(opts?: ConsentChoiceOptions): Promise<void>;
190
+ declineConsent(opts?: ConsentChoiceOptions): void;
191
+ getConsentState(): ConsentState;
192
+ onConsentChange(cb: (state: ConsentState) => void): () => void;
193
+ /** @deprecated Use `grantConsent()`. */
170
194
  consent(): Promise<void>;
171
195
  flush(): Promise<void>;
172
196
  close(): void;
@@ -189,6 +213,11 @@ declare function setDeploymentContext(context: DeploymentContext): void;
189
213
  declare function setFeatureFlags(featureFlags: FeatureFlagsContext): void;
190
214
  declare function setTags(tags: Record<string, string>): void;
191
215
  declare function flush(): Promise<void>;
216
+ declare function grantConsent(options?: ConsentChoiceOptions): Promise<void>;
217
+ declare function declineConsent(options?: ConsentChoiceOptions): void;
218
+ declare function getConsentState(): ConsentState;
219
+ declare function onConsentChange(cb: (state: ConsentState) => void): () => void;
220
+ /** @deprecated Use `grantConsent()`. */
192
221
  declare function consent(): Promise<void>;
193
222
  declare function group(groupId: string, traits?: Record<string, unknown>): void;
194
223
  declare function evaluateFlags(options: FlagEvaluationOptions): Promise<FeatureFlagsContext>;
@@ -196,4 +225,4 @@ declare function getFlag<T extends string | number | boolean | null>(flagKey: st
196
225
  declare function refreshFlags(options: FlagEvaluationOptions): Promise<FeatureFlagsContext>;
197
226
  declare function captureExposure(flagKey: string, variantKey: string, variantValue?: string | number | boolean | null, reason?: string, environment?: string): void;
198
227
 
199
- export { type AutoCaptureOptions, type CaptureOptions, type DeploymentContext, type DevToolsMessage, type EmitVisionOptions, type EmitVisionTraits, type EmitVisionUser, type FeatureFlagsContext, type FlagEvaluationOptions, captureError, captureEvent, captureExposure, captureFeedback, capturePageView, consent, evaluateFlags, flush, getFlag, group, identify, init, refreshFlags, setContext, setDeploymentContext, setFeatureFlags, setTags };
228
+ export { type AutoCaptureOptions, type CaptureOptions, type ConsentChoiceOptions, type ConsentState, type ConsentStorage, type DeploymentContext, type DevToolsMessage, type EmitVisionOptions, type EmitVisionTraits, type EmitVisionUser, type FeatureFlagsContext, type FlagEvaluationOptions, captureError, captureEvent, captureExposure, captureFeedback, capturePageView, consent, consentStorageKey, declineConsent, evaluateFlags, flush, getConsentState, getFlag, grantConsent, group, identify, init, onConsentChange, refreshFlags, setContext, setDeploymentContext, setFeatureFlags, setTags };