@shipeasy/sdk 2.2.0 → 2.4.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.
@@ -28,11 +28,23 @@ interface EvalResponse {
28
28
  configs: Record<string, unknown>;
29
29
  experiments: Record<string, EvalExpResult>;
30
30
  }
31
+ interface AutoCollectGroups {
32
+ vitals: boolean;
33
+ errors: boolean;
34
+ engagement: boolean;
35
+ }
31
36
  type FlagsClientBrowserEnv = "dev" | "staging" | "prod";
32
37
  interface FlagsClientBrowserOptions {
33
38
  sdkKey: string;
34
39
  baseUrl?: string;
35
40
  autoGuardrails?: boolean;
41
+ /**
42
+ * Per-group enablement for auto-collected metrics. When set, overrides the
43
+ * blanket `autoGuardrails` flag for the specific groups listed. Any group
44
+ * not present in the object falls back to `autoGuardrails` (defaulting to
45
+ * true when `autoGuardrails` is true).
46
+ */
47
+ autoGuardrailGroups?: Partial<AutoCollectGroups>;
36
48
  /** Which published env to read values from. Defaults to "prod". */
37
49
  env?: FlagsClientBrowserEnv;
38
50
  }
@@ -40,6 +52,7 @@ declare class FlagsClientBrowser {
40
52
  private readonly sdkKey;
41
53
  private readonly baseUrl;
42
54
  private readonly autoGuardrails;
55
+ private readonly autoGuardrailGroups;
43
56
  private readonly env;
44
57
  private evalResult;
45
58
  private anonId;
@@ -125,11 +138,20 @@ interface ShipeasyClientConfig {
125
138
  */
126
139
  autoIdentify?: boolean;
127
140
  /**
128
- * Capture web vitals (LCP, CLS, INP, TTFB, FCP) and JS/network errors as
129
- * `__auto_*` metric events. Defaults to false (opt-in) enabling requires the
130
- * `__auto_*` event names to be approved in the project event catalog.
141
+ * Capture web vitals (LCP, CLS, INP, TTFB, FCP, navigation timing), JS /
142
+ * network errors, and engagement signals (abandonment) as `__auto_*`
143
+ * metric events. Defaults to `true` the worker bypasses event-catalog
144
+ * validation for `__auto_*` names so this is safe out of the box.
145
+ *
146
+ * Pass `false` to disable everything, or a per-group object to narrow:
147
+ *
148
+ * ```ts
149
+ * shipeasy({ apiKey, autoCollect: false }); // off
150
+ * shipeasy({ apiKey, autoCollect: { errors: false } }); // vitals + engagement only
151
+ * shipeasy({ apiKey }); // all groups on
152
+ * ```
131
153
  */
132
- autoCollect?: boolean;
154
+ autoCollect?: boolean | Partial<AutoCollectGroups>;
133
155
  }
134
156
  /**
135
157
  * Initialise the ShipEasy client SDK and wire up lazy devtools.
@@ -237,4 +259,4 @@ interface I18nFacade {
237
259
  }
238
260
  declare const i18n: I18nFacade;
239
261
 
240
- export { type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, shipeasy, version };
262
+ export { type AutoCollectGroups, type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, shipeasy, version };
@@ -28,11 +28,23 @@ interface EvalResponse {
28
28
  configs: Record<string, unknown>;
29
29
  experiments: Record<string, EvalExpResult>;
30
30
  }
31
+ interface AutoCollectGroups {
32
+ vitals: boolean;
33
+ errors: boolean;
34
+ engagement: boolean;
35
+ }
31
36
  type FlagsClientBrowserEnv = "dev" | "staging" | "prod";
32
37
  interface FlagsClientBrowserOptions {
33
38
  sdkKey: string;
34
39
  baseUrl?: string;
35
40
  autoGuardrails?: boolean;
41
+ /**
42
+ * Per-group enablement for auto-collected metrics. When set, overrides the
43
+ * blanket `autoGuardrails` flag for the specific groups listed. Any group
44
+ * not present in the object falls back to `autoGuardrails` (defaulting to
45
+ * true when `autoGuardrails` is true).
46
+ */
47
+ autoGuardrailGroups?: Partial<AutoCollectGroups>;
36
48
  /** Which published env to read values from. Defaults to "prod". */
37
49
  env?: FlagsClientBrowserEnv;
38
50
  }
@@ -40,6 +52,7 @@ declare class FlagsClientBrowser {
40
52
  private readonly sdkKey;
41
53
  private readonly baseUrl;
42
54
  private readonly autoGuardrails;
55
+ private readonly autoGuardrailGroups;
43
56
  private readonly env;
44
57
  private evalResult;
45
58
  private anonId;
@@ -125,11 +138,20 @@ interface ShipeasyClientConfig {
125
138
  */
126
139
  autoIdentify?: boolean;
127
140
  /**
128
- * Capture web vitals (LCP, CLS, INP, TTFB, FCP) and JS/network errors as
129
- * `__auto_*` metric events. Defaults to false (opt-in) enabling requires the
130
- * `__auto_*` event names to be approved in the project event catalog.
141
+ * Capture web vitals (LCP, CLS, INP, TTFB, FCP, navigation timing), JS /
142
+ * network errors, and engagement signals (abandonment) as `__auto_*`
143
+ * metric events. Defaults to `true` the worker bypasses event-catalog
144
+ * validation for `__auto_*` names so this is safe out of the box.
145
+ *
146
+ * Pass `false` to disable everything, or a per-group object to narrow:
147
+ *
148
+ * ```ts
149
+ * shipeasy({ apiKey, autoCollect: false }); // off
150
+ * shipeasy({ apiKey, autoCollect: { errors: false } }); // vitals + engagement only
151
+ * shipeasy({ apiKey }); // all groups on
152
+ * ```
131
153
  */
132
- autoCollect?: boolean;
154
+ autoCollect?: boolean | Partial<AutoCollectGroups>;
133
155
  }
134
156
  /**
135
157
  * Initialise the ShipEasy client SDK and wire up lazy devtools.
@@ -237,4 +259,4 @@ interface I18nFacade {
237
259
  }
238
260
  declare const i18n: I18nFacade;
239
261
 
240
- export { type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, shipeasy, version };
262
+ export { type AutoCollectGroups, type BootstrapPayload, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, shipeasy, version };
@@ -167,7 +167,7 @@ var EventBuffer = class {
167
167
  }
168
168
  };
169
169
  var MAX_ERRORS_PER_SESSION = 5;
170
- function installAutoGuardrails(buffer, userId, anonId) {
170
+ function installAutoGuardrails(buffer, userId, anonId, groups) {
171
171
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
172
172
  let lcp = null;
173
173
  let inp = null;
@@ -175,100 +175,105 @@ function installAutoGuardrails(buffer, userId, anonId) {
175
175
  let jsErrorCount = 0;
176
176
  let netErrorCount = 0;
177
177
  let navTimingFlushed = false;
178
- try {
179
- const lcpObs = new PerformanceObserver((list) => {
180
- const entries = list.getEntries();
181
- if (entries.length)
182
- lcp = entries[entries.length - 1].startTime;
183
- });
184
- lcpObs.observe({ type: "largest-contentful-paint", buffered: true });
185
- } catch {
186
- }
187
- try {
188
- const inpObs = new PerformanceObserver((list) => {
189
- for (const e of list.getEntries()) {
190
- const dur = e.duration ?? 0;
191
- if (inp === null || dur > inp) inp = dur;
192
- }
193
- });
194
- inpObs.observe({
195
- type: "event",
196
- buffered: true,
197
- durationThreshold: 16
198
- });
199
- } catch {
200
- }
201
- try {
202
- const clsObs = new PerformanceObserver((list) => {
203
- for (const e of list.getEntries()) {
204
- if (e.value > 0.1) clsBad = true;
205
- }
206
- });
207
- clsObs.observe({ type: "layout-shift", buffered: true });
208
- } catch {
209
- }
210
- const origOnError = window.onerror;
211
- window.onerror = (msg, source, lineno, _colno, err) => {
212
- if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
213
- jsErrorCount += 1;
214
- buffer.pushMetric("__auto_js_error", userId, anonId, {
215
- value: 1,
216
- kind: "exception",
217
- message: typeof msg === "string" ? msg.slice(0, 200) : String(err ?? "").slice(0, 200),
218
- source: typeof source === "string" ? source.slice(0, 200) : "",
219
- line: lineno ?? 0
178
+ if (groups.vitals) {
179
+ try {
180
+ const lcpObs = new PerformanceObserver((list) => {
181
+ const entries = list.getEntries();
182
+ if (entries.length)
183
+ lcp = entries[entries.length - 1].startTime;
220
184
  });
185
+ lcpObs.observe({ type: "largest-contentful-paint", buffered: true });
186
+ } catch {
221
187
  }
222
- if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
223
- return false;
224
- };
225
- window.addEventListener("unhandledrejection", (e) => {
226
- if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
227
- jsErrorCount += 1;
228
- const reason = e.reason;
229
- const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : String(reason);
230
- buffer.pushMetric("__auto_js_error", userId, anonId, {
231
- value: 1,
232
- kind: "unhandled_rejection",
233
- message: message.slice(0, 200)
188
+ try {
189
+ const inpObs = new PerformanceObserver((list) => {
190
+ for (const e of list.getEntries()) {
191
+ const dur = e.duration ?? 0;
192
+ if (inp === null || dur > inp) inp = dur;
193
+ }
194
+ });
195
+ inpObs.observe({
196
+ type: "event",
197
+ buffered: true,
198
+ durationThreshold: 16
234
199
  });
200
+ } catch {
235
201
  }
236
- });
237
- const origFetch = window.fetch;
238
- window.fetch = async function(...args) {
239
- const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
240
- const url = typeof args[0] === "string" ? args[0] : args[0].toString();
241
- let res;
242
202
  try {
243
- res = await origFetch.apply(this, args);
244
- } catch (err) {
245
- if (netErrorCount < MAX_ERRORS_PER_SESSION) {
203
+ const clsObs = new PerformanceObserver((list) => {
204
+ for (const e of list.getEntries()) {
205
+ if (e.value > 0.1) clsBad = true;
206
+ }
207
+ });
208
+ clsObs.observe({ type: "layout-shift", buffered: true });
209
+ } catch {
210
+ }
211
+ }
212
+ if (groups.errors) {
213
+ const origOnError = window.onerror;
214
+ window.onerror = (msg, source, lineno, _colno, err) => {
215
+ if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
216
+ jsErrorCount += 1;
217
+ buffer.pushMetric("__auto_js_error", userId, anonId, {
218
+ value: 1,
219
+ kind: "exception",
220
+ message: typeof msg === "string" ? msg.slice(0, 200) : String(err ?? "").slice(0, 200),
221
+ source: typeof source === "string" ? source.slice(0, 200) : "",
222
+ line: lineno ?? 0
223
+ });
224
+ }
225
+ if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
226
+ return false;
227
+ };
228
+ window.addEventListener("unhandledrejection", (e) => {
229
+ if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
230
+ jsErrorCount += 1;
231
+ const reason = e.reason;
232
+ const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : String(reason);
233
+ buffer.pushMetric("__auto_js_error", userId, anonId, {
234
+ value: 1,
235
+ kind: "unhandled_rejection",
236
+ message: message.slice(0, 200)
237
+ });
238
+ }
239
+ });
240
+ const origFetch = window.fetch;
241
+ window.fetch = async function(...args) {
242
+ const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
243
+ const url = typeof args[0] === "string" ? args[0] : args[0].toString();
244
+ let res;
245
+ try {
246
+ res = await origFetch.apply(this, args);
247
+ } catch (err) {
248
+ if (netErrorCount < MAX_ERRORS_PER_SESSION) {
249
+ netErrorCount += 1;
250
+ buffer.pushMetric("__auto_network_error", userId, anonId, {
251
+ value: 1,
252
+ kind: "network",
253
+ status: 0,
254
+ url: url.slice(0, 200)
255
+ });
256
+ }
257
+ throw err;
258
+ }
259
+ if (res.status >= 500 && netErrorCount < MAX_ERRORS_PER_SESSION) {
246
260
  netErrorCount += 1;
261
+ const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
247
262
  buffer.pushMetric("__auto_network_error", userId, anonId, {
248
263
  value: 1,
249
- kind: "network",
250
- status: 0,
251
- url: url.slice(0, 200)
264
+ kind: "5xx",
265
+ status: res.status,
266
+ url: url.slice(0, 200),
267
+ duration_ms: Math.round(elapsed)
252
268
  });
253
269
  }
254
- throw err;
255
- }
256
- if (res.status >= 500 && netErrorCount < MAX_ERRORS_PER_SESSION) {
257
- netErrorCount += 1;
258
- const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
259
- buffer.pushMetric("__auto_network_error", userId, anonId, {
260
- value: 1,
261
- kind: "5xx",
262
- status: res.status,
263
- url: url.slice(0, 200),
264
- duration_ms: Math.round(elapsed)
265
- });
266
- }
267
- return res;
268
- };
270
+ return res;
271
+ };
272
+ }
269
273
  const flushNavTiming = () => {
270
274
  if (navTimingFlushed) return;
271
275
  navTimingFlushed = true;
276
+ if (!groups.vitals) return;
272
277
  try {
273
278
  const navList = performance.getEntriesByType("navigation");
274
279
  const nav = navList[0];
@@ -301,29 +306,53 @@ function installAutoGuardrails(buffer, userId, anonId) {
301
306
  } catch {
302
307
  }
303
308
  };
304
- if (document.readyState === "complete") {
305
- setTimeout(flushNavTiming, 0);
306
- } else {
307
- window.addEventListener(
308
- "load",
309
- () => {
310
- setTimeout(flushNavTiming, 0);
311
- },
312
- { once: true }
313
- );
309
+ if (groups.engagement) {
310
+ try {
311
+ buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
312
+ } catch {
313
+ }
314
+ let lastEmit = Date.now();
315
+ const SESSION_GAP_MS = 30 * 60 * 1e3;
316
+ document.addEventListener("visibilitychange", () => {
317
+ if (document.visibilityState !== "visible") return;
318
+ if (Date.now() - lastEmit < SESSION_GAP_MS) return;
319
+ try {
320
+ buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
321
+ lastEmit = Date.now();
322
+ } catch {
323
+ }
324
+ });
325
+ }
326
+ const needHide = groups.vitals || groups.engagement;
327
+ if (needHide) {
328
+ if (document.readyState === "complete") {
329
+ setTimeout(flushNavTiming, 0);
330
+ } else {
331
+ window.addEventListener(
332
+ "load",
333
+ () => {
334
+ setTimeout(flushNavTiming, 0);
335
+ },
336
+ { once: true }
337
+ );
338
+ }
339
+ const flushOnHide = () => {
340
+ flushNavTiming();
341
+ if (groups.vitals) {
342
+ if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
343
+ if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
344
+ if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
345
+ }
346
+ if (groups.engagement) {
347
+ const abandoned = lcp === null ? 1 : 0;
348
+ buffer.pushMetric("__auto_abandoned", userId, anonId, { value: abandoned });
349
+ }
350
+ buffer.flush(true);
351
+ };
352
+ document.addEventListener("visibilitychange", () => {
353
+ if (document.visibilityState === "hidden") flushOnHide();
354
+ });
314
355
  }
315
- const flushOnHide = () => {
316
- flushNavTiming();
317
- if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
318
- if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
319
- if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
320
- const abandoned = lcp === null ? 1 : 0;
321
- buffer.pushMetric("__auto_abandoned", userId, anonId, { value: abandoned });
322
- buffer.flush(true);
323
- };
324
- document.addEventListener("visibilitychange", () => {
325
- if (document.visibilityState === "hidden") flushOnHide();
326
- });
327
356
  }
328
357
  function getOrCreateAnonId() {
329
358
  try {
@@ -391,6 +420,7 @@ var FlagsClientBrowser = class {
391
420
  sdkKey;
392
421
  baseUrl;
393
422
  autoGuardrails;
423
+ autoGuardrailGroups;
394
424
  env;
395
425
  evalResult = null;
396
426
  anonId;
@@ -410,7 +440,13 @@ var FlagsClientBrowser = class {
410
440
  this.sdkKey = opts.sdkKey;
411
441
  this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
412
442
  this.env = opts.env ?? "prod";
413
- this.autoGuardrails = opts.autoGuardrails === true;
443
+ this.autoGuardrails = opts.autoGuardrails !== false;
444
+ const g = opts.autoGuardrailGroups ?? {};
445
+ this.autoGuardrailGroups = {
446
+ vitals: g.vitals ?? this.autoGuardrails,
447
+ errors: g.errors ?? this.autoGuardrails,
448
+ engagement: g.engagement ?? this.autoGuardrails
449
+ };
414
450
  this.anonId = getOrCreateAnonId();
415
451
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
416
452
  void this.buffer.flushPendingAlias();
@@ -439,9 +475,10 @@ var FlagsClientBrowser = class {
439
475
  const data = await res.json();
440
476
  if (seq !== this.identifySeq) return;
441
477
  this.evalResult = data;
442
- if (this.autoGuardrails && !this.guardrailsInstalled) {
478
+ const anyGroupOn = this.autoGuardrailGroups.vitals || this.autoGuardrailGroups.errors || this.autoGuardrailGroups.engagement;
479
+ if (anyGroupOn && !this.guardrailsInstalled) {
443
480
  this.guardrailsInstalled = true;
444
- installAutoGuardrails(this.buffer, this.userId, this.anonId);
481
+ installAutoGuardrails(this.buffer, this.userId, this.anonId, this.autoGuardrailGroups);
445
482
  }
446
483
  this.notify();
447
484
  }
@@ -657,10 +694,14 @@ function attachDevtools(client, opts = {}) {
657
694
  }
658
695
  var _client = null;
659
696
  function shipeasy(opts) {
697
+ const ac = opts.autoCollect;
698
+ const blanket = ac === false ? false : true;
699
+ const groups = ac && typeof ac === "object" ? ac : void 0;
660
700
  const client = configureShipeasy({
661
701
  sdkKey: opts.apiKey,
662
702
  baseUrl: opts.baseUrl ?? "https://cdn.shipeasy.ai",
663
- autoGuardrails: opts.autoCollect === true
703
+ autoGuardrails: blanket,
704
+ autoGuardrailGroups: groups
664
705
  });
665
706
  flags.notifyMounted();
666
707
  if (opts.autoIdentify !== false) {
@@ -124,7 +124,7 @@ var EventBuffer = class {
124
124
  }
125
125
  };
126
126
  var MAX_ERRORS_PER_SESSION = 5;
127
- function installAutoGuardrails(buffer, userId, anonId) {
127
+ function installAutoGuardrails(buffer, userId, anonId, groups) {
128
128
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
129
129
  let lcp = null;
130
130
  let inp = null;
@@ -132,100 +132,105 @@ function installAutoGuardrails(buffer, userId, anonId) {
132
132
  let jsErrorCount = 0;
133
133
  let netErrorCount = 0;
134
134
  let navTimingFlushed = false;
135
- try {
136
- const lcpObs = new PerformanceObserver((list) => {
137
- const entries = list.getEntries();
138
- if (entries.length)
139
- lcp = entries[entries.length - 1].startTime;
140
- });
141
- lcpObs.observe({ type: "largest-contentful-paint", buffered: true });
142
- } catch {
143
- }
144
- try {
145
- const inpObs = new PerformanceObserver((list) => {
146
- for (const e of list.getEntries()) {
147
- const dur = e.duration ?? 0;
148
- if (inp === null || dur > inp) inp = dur;
149
- }
150
- });
151
- inpObs.observe({
152
- type: "event",
153
- buffered: true,
154
- durationThreshold: 16
155
- });
156
- } catch {
157
- }
158
- try {
159
- const clsObs = new PerformanceObserver((list) => {
160
- for (const e of list.getEntries()) {
161
- if (e.value > 0.1) clsBad = true;
162
- }
163
- });
164
- clsObs.observe({ type: "layout-shift", buffered: true });
165
- } catch {
166
- }
167
- const origOnError = window.onerror;
168
- window.onerror = (msg, source, lineno, _colno, err) => {
169
- if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
170
- jsErrorCount += 1;
171
- buffer.pushMetric("__auto_js_error", userId, anonId, {
172
- value: 1,
173
- kind: "exception",
174
- message: typeof msg === "string" ? msg.slice(0, 200) : String(err ?? "").slice(0, 200),
175
- source: typeof source === "string" ? source.slice(0, 200) : "",
176
- line: lineno ?? 0
135
+ if (groups.vitals) {
136
+ try {
137
+ const lcpObs = new PerformanceObserver((list) => {
138
+ const entries = list.getEntries();
139
+ if (entries.length)
140
+ lcp = entries[entries.length - 1].startTime;
177
141
  });
142
+ lcpObs.observe({ type: "largest-contentful-paint", buffered: true });
143
+ } catch {
178
144
  }
179
- if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
180
- return false;
181
- };
182
- window.addEventListener("unhandledrejection", (e) => {
183
- if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
184
- jsErrorCount += 1;
185
- const reason = e.reason;
186
- const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : String(reason);
187
- buffer.pushMetric("__auto_js_error", userId, anonId, {
188
- value: 1,
189
- kind: "unhandled_rejection",
190
- message: message.slice(0, 200)
145
+ try {
146
+ const inpObs = new PerformanceObserver((list) => {
147
+ for (const e of list.getEntries()) {
148
+ const dur = e.duration ?? 0;
149
+ if (inp === null || dur > inp) inp = dur;
150
+ }
151
+ });
152
+ inpObs.observe({
153
+ type: "event",
154
+ buffered: true,
155
+ durationThreshold: 16
191
156
  });
157
+ } catch {
192
158
  }
193
- });
194
- const origFetch = window.fetch;
195
- window.fetch = async function(...args) {
196
- const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
197
- const url = typeof args[0] === "string" ? args[0] : args[0].toString();
198
- let res;
199
159
  try {
200
- res = await origFetch.apply(this, args);
201
- } catch (err) {
202
- if (netErrorCount < MAX_ERRORS_PER_SESSION) {
160
+ const clsObs = new PerformanceObserver((list) => {
161
+ for (const e of list.getEntries()) {
162
+ if (e.value > 0.1) clsBad = true;
163
+ }
164
+ });
165
+ clsObs.observe({ type: "layout-shift", buffered: true });
166
+ } catch {
167
+ }
168
+ }
169
+ if (groups.errors) {
170
+ const origOnError = window.onerror;
171
+ window.onerror = (msg, source, lineno, _colno, err) => {
172
+ if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
173
+ jsErrorCount += 1;
174
+ buffer.pushMetric("__auto_js_error", userId, anonId, {
175
+ value: 1,
176
+ kind: "exception",
177
+ message: typeof msg === "string" ? msg.slice(0, 200) : String(err ?? "").slice(0, 200),
178
+ source: typeof source === "string" ? source.slice(0, 200) : "",
179
+ line: lineno ?? 0
180
+ });
181
+ }
182
+ if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
183
+ return false;
184
+ };
185
+ window.addEventListener("unhandledrejection", (e) => {
186
+ if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
187
+ jsErrorCount += 1;
188
+ const reason = e.reason;
189
+ const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : String(reason);
190
+ buffer.pushMetric("__auto_js_error", userId, anonId, {
191
+ value: 1,
192
+ kind: "unhandled_rejection",
193
+ message: message.slice(0, 200)
194
+ });
195
+ }
196
+ });
197
+ const origFetch = window.fetch;
198
+ window.fetch = async function(...args) {
199
+ const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
200
+ const url = typeof args[0] === "string" ? args[0] : args[0].toString();
201
+ let res;
202
+ try {
203
+ res = await origFetch.apply(this, args);
204
+ } catch (err) {
205
+ if (netErrorCount < MAX_ERRORS_PER_SESSION) {
206
+ netErrorCount += 1;
207
+ buffer.pushMetric("__auto_network_error", userId, anonId, {
208
+ value: 1,
209
+ kind: "network",
210
+ status: 0,
211
+ url: url.slice(0, 200)
212
+ });
213
+ }
214
+ throw err;
215
+ }
216
+ if (res.status >= 500 && netErrorCount < MAX_ERRORS_PER_SESSION) {
203
217
  netErrorCount += 1;
218
+ const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
204
219
  buffer.pushMetric("__auto_network_error", userId, anonId, {
205
220
  value: 1,
206
- kind: "network",
207
- status: 0,
208
- url: url.slice(0, 200)
221
+ kind: "5xx",
222
+ status: res.status,
223
+ url: url.slice(0, 200),
224
+ duration_ms: Math.round(elapsed)
209
225
  });
210
226
  }
211
- throw err;
212
- }
213
- if (res.status >= 500 && netErrorCount < MAX_ERRORS_PER_SESSION) {
214
- netErrorCount += 1;
215
- const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
216
- buffer.pushMetric("__auto_network_error", userId, anonId, {
217
- value: 1,
218
- kind: "5xx",
219
- status: res.status,
220
- url: url.slice(0, 200),
221
- duration_ms: Math.round(elapsed)
222
- });
223
- }
224
- return res;
225
- };
227
+ return res;
228
+ };
229
+ }
226
230
  const flushNavTiming = () => {
227
231
  if (navTimingFlushed) return;
228
232
  navTimingFlushed = true;
233
+ if (!groups.vitals) return;
229
234
  try {
230
235
  const navList = performance.getEntriesByType("navigation");
231
236
  const nav = navList[0];
@@ -258,29 +263,53 @@ function installAutoGuardrails(buffer, userId, anonId) {
258
263
  } catch {
259
264
  }
260
265
  };
261
- if (document.readyState === "complete") {
262
- setTimeout(flushNavTiming, 0);
263
- } else {
264
- window.addEventListener(
265
- "load",
266
- () => {
267
- setTimeout(flushNavTiming, 0);
268
- },
269
- { once: true }
270
- );
271
- }
272
- const flushOnHide = () => {
273
- flushNavTiming();
274
- if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
275
- if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
276
- if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
277
- const abandoned = lcp === null ? 1 : 0;
278
- buffer.pushMetric("__auto_abandoned", userId, anonId, { value: abandoned });
279
- buffer.flush(true);
280
- };
281
- document.addEventListener("visibilitychange", () => {
282
- if (document.visibilityState === "hidden") flushOnHide();
283
- });
266
+ if (groups.engagement) {
267
+ try {
268
+ buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
269
+ } catch {
270
+ }
271
+ let lastEmit = Date.now();
272
+ const SESSION_GAP_MS = 30 * 60 * 1e3;
273
+ document.addEventListener("visibilitychange", () => {
274
+ if (document.visibilityState !== "visible") return;
275
+ if (Date.now() - lastEmit < SESSION_GAP_MS) return;
276
+ try {
277
+ buffer.pushMetric("__auto_session_active", userId, anonId, { value: 1 });
278
+ lastEmit = Date.now();
279
+ } catch {
280
+ }
281
+ });
282
+ }
283
+ const needHide = groups.vitals || groups.engagement;
284
+ if (needHide) {
285
+ if (document.readyState === "complete") {
286
+ setTimeout(flushNavTiming, 0);
287
+ } else {
288
+ window.addEventListener(
289
+ "load",
290
+ () => {
291
+ setTimeout(flushNavTiming, 0);
292
+ },
293
+ { once: true }
294
+ );
295
+ }
296
+ const flushOnHide = () => {
297
+ flushNavTiming();
298
+ if (groups.vitals) {
299
+ if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
300
+ if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
301
+ if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
302
+ }
303
+ if (groups.engagement) {
304
+ const abandoned = lcp === null ? 1 : 0;
305
+ buffer.pushMetric("__auto_abandoned", userId, anonId, { value: abandoned });
306
+ }
307
+ buffer.flush(true);
308
+ };
309
+ document.addEventListener("visibilitychange", () => {
310
+ if (document.visibilityState === "hidden") flushOnHide();
311
+ });
312
+ }
284
313
  }
285
314
  function getOrCreateAnonId() {
286
315
  try {
@@ -348,6 +377,7 @@ var FlagsClientBrowser = class {
348
377
  sdkKey;
349
378
  baseUrl;
350
379
  autoGuardrails;
380
+ autoGuardrailGroups;
351
381
  env;
352
382
  evalResult = null;
353
383
  anonId;
@@ -367,7 +397,13 @@ var FlagsClientBrowser = class {
367
397
  this.sdkKey = opts.sdkKey;
368
398
  this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
369
399
  this.env = opts.env ?? "prod";
370
- this.autoGuardrails = opts.autoGuardrails === true;
400
+ this.autoGuardrails = opts.autoGuardrails !== false;
401
+ const g = opts.autoGuardrailGroups ?? {};
402
+ this.autoGuardrailGroups = {
403
+ vitals: g.vitals ?? this.autoGuardrails,
404
+ errors: g.errors ?? this.autoGuardrails,
405
+ engagement: g.engagement ?? this.autoGuardrails
406
+ };
371
407
  this.anonId = getOrCreateAnonId();
372
408
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
373
409
  void this.buffer.flushPendingAlias();
@@ -396,9 +432,10 @@ var FlagsClientBrowser = class {
396
432
  const data = await res.json();
397
433
  if (seq !== this.identifySeq) return;
398
434
  this.evalResult = data;
399
- if (this.autoGuardrails && !this.guardrailsInstalled) {
435
+ const anyGroupOn = this.autoGuardrailGroups.vitals || this.autoGuardrailGroups.errors || this.autoGuardrailGroups.engagement;
436
+ if (anyGroupOn && !this.guardrailsInstalled) {
400
437
  this.guardrailsInstalled = true;
401
- installAutoGuardrails(this.buffer, this.userId, this.anonId);
438
+ installAutoGuardrails(this.buffer, this.userId, this.anonId, this.autoGuardrailGroups);
402
439
  }
403
440
  this.notify();
404
441
  }
@@ -614,10 +651,14 @@ function attachDevtools(client, opts = {}) {
614
651
  }
615
652
  var _client = null;
616
653
  function shipeasy(opts) {
654
+ const ac = opts.autoCollect;
655
+ const blanket = ac === false ? false : true;
656
+ const groups = ac && typeof ac === "object" ? ac : void 0;
617
657
  const client = configureShipeasy({
618
658
  sdkKey: opts.apiKey,
619
659
  baseUrl: opts.baseUrl ?? "https://cdn.shipeasy.ai",
620
- autoGuardrails: opts.autoCollect === true
660
+ autoGuardrails: blanket,
661
+ autoGuardrailGroups: groups
621
662
  });
622
663
  flags.notifyMounted();
623
664
  if (opts.autoIdentify !== false) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://shipeasy.ai",