@shipeasy/sdk 2.1.15 → 2.3.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;
@@ -124,6 +137,21 @@ interface ShipeasyClientConfig {
124
137
  * avoid the initial anon /sdk/evaluate round-trip.
125
138
  */
126
139
  autoIdentify?: boolean;
140
+ /**
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
+ * ```
153
+ */
154
+ autoCollect?: boolean | Partial<AutoCollectGroups>;
127
155
  }
128
156
  /**
129
157
  * Initialise the ShipEasy client SDK and wire up lazy devtools.
@@ -231,4 +259,4 @@ interface I18nFacade {
231
259
  }
232
260
  declare const i18n: I18nFacade;
233
261
 
234
- 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;
@@ -124,6 +137,21 @@ interface ShipeasyClientConfig {
124
137
  * avoid the initial anon /sdk/evaluate round-trip.
125
138
  */
126
139
  autoIdentify?: boolean;
140
+ /**
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
+ * ```
153
+ */
154
+ autoCollect?: boolean | Partial<AutoCollectGroups>;
127
155
  }
128
156
  /**
129
157
  * Initialise the ShipEasy client SDK and wire up lazy devtools.
@@ -231,4 +259,4 @@ interface I18nFacade {
231
259
  }
232
260
  declare const i18n: I18nFacade;
233
261
 
234
- 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
+ }
234
194
  });
195
+ inpObs.observe({
196
+ type: "event",
197
+ buffered: true,
198
+ durationThreshold: 16
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,36 @@ 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
+ const needHide = groups.vitals || groups.engagement;
310
+ if (needHide) {
311
+ if (document.readyState === "complete") {
312
+ setTimeout(flushNavTiming, 0);
313
+ } else {
314
+ window.addEventListener(
315
+ "load",
316
+ () => {
317
+ setTimeout(flushNavTiming, 0);
318
+ },
319
+ { once: true }
320
+ );
321
+ }
322
+ const flushOnHide = () => {
323
+ flushNavTiming();
324
+ if (groups.vitals) {
325
+ if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
326
+ if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
327
+ if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
328
+ }
329
+ if (groups.engagement) {
330
+ const abandoned = lcp === null ? 1 : 0;
331
+ buffer.pushMetric("__auto_abandoned", userId, anonId, { value: abandoned });
332
+ }
333
+ buffer.flush(true);
334
+ };
335
+ document.addEventListener("visibilitychange", () => {
336
+ if (document.visibilityState === "hidden") flushOnHide();
337
+ });
314
338
  }
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
339
  }
328
340
  function getOrCreateAnonId() {
329
341
  try {
@@ -391,6 +403,7 @@ var FlagsClientBrowser = class {
391
403
  sdkKey;
392
404
  baseUrl;
393
405
  autoGuardrails;
406
+ autoGuardrailGroups;
394
407
  env;
395
408
  evalResult = null;
396
409
  anonId;
@@ -411,6 +424,12 @@ var FlagsClientBrowser = class {
411
424
  this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
412
425
  this.env = opts.env ?? "prod";
413
426
  this.autoGuardrails = opts.autoGuardrails !== false;
427
+ const g = opts.autoGuardrailGroups ?? {};
428
+ this.autoGuardrailGroups = {
429
+ vitals: g.vitals ?? this.autoGuardrails,
430
+ errors: g.errors ?? this.autoGuardrails,
431
+ engagement: g.engagement ?? this.autoGuardrails
432
+ };
414
433
  this.anonId = getOrCreateAnonId();
415
434
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
416
435
  void this.buffer.flushPendingAlias();
@@ -439,9 +458,10 @@ var FlagsClientBrowser = class {
439
458
  const data = await res.json();
440
459
  if (seq !== this.identifySeq) return;
441
460
  this.evalResult = data;
442
- if (this.autoGuardrails && !this.guardrailsInstalled) {
461
+ const anyGroupOn = this.autoGuardrailGroups.vitals || this.autoGuardrailGroups.errors || this.autoGuardrailGroups.engagement;
462
+ if (anyGroupOn && !this.guardrailsInstalled) {
443
463
  this.guardrailsInstalled = true;
444
- installAutoGuardrails(this.buffer, this.userId, this.anonId);
464
+ installAutoGuardrails(this.buffer, this.userId, this.anonId, this.autoGuardrailGroups);
445
465
  }
446
466
  this.notify();
447
467
  }
@@ -657,9 +677,14 @@ function attachDevtools(client, opts = {}) {
657
677
  }
658
678
  var _client = null;
659
679
  function shipeasy(opts) {
680
+ const ac = opts.autoCollect;
681
+ const blanket = ac === false ? false : true;
682
+ const groups = ac && typeof ac === "object" ? ac : void 0;
660
683
  const client = configureShipeasy({
661
684
  sdkKey: opts.apiKey,
662
- baseUrl: opts.baseUrl ?? "https://cdn.shipeasy.ai"
685
+ baseUrl: opts.baseUrl ?? "https://cdn.shipeasy.ai",
686
+ autoGuardrails: blanket,
687
+ autoGuardrailGroups: groups
663
688
  });
664
689
  flags.notifyMounted();
665
690
  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
+ }
191
151
  });
152
+ inpObs.observe({
153
+ type: "event",
154
+ buffered: true,
155
+ durationThreshold: 16
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,36 @@ 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
+ const needHide = groups.vitals || groups.engagement;
267
+ if (needHide) {
268
+ if (document.readyState === "complete") {
269
+ setTimeout(flushNavTiming, 0);
270
+ } else {
271
+ window.addEventListener(
272
+ "load",
273
+ () => {
274
+ setTimeout(flushNavTiming, 0);
275
+ },
276
+ { once: true }
277
+ );
278
+ }
279
+ const flushOnHide = () => {
280
+ flushNavTiming();
281
+ if (groups.vitals) {
282
+ if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
283
+ if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
284
+ if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
285
+ }
286
+ if (groups.engagement) {
287
+ const abandoned = lcp === null ? 1 : 0;
288
+ buffer.pushMetric("__auto_abandoned", userId, anonId, { value: abandoned });
289
+ }
290
+ buffer.flush(true);
291
+ };
292
+ document.addEventListener("visibilitychange", () => {
293
+ if (document.visibilityState === "hidden") flushOnHide();
294
+ });
295
+ }
284
296
  }
285
297
  function getOrCreateAnonId() {
286
298
  try {
@@ -348,6 +360,7 @@ var FlagsClientBrowser = class {
348
360
  sdkKey;
349
361
  baseUrl;
350
362
  autoGuardrails;
363
+ autoGuardrailGroups;
351
364
  env;
352
365
  evalResult = null;
353
366
  anonId;
@@ -368,6 +381,12 @@ var FlagsClientBrowser = class {
368
381
  this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
369
382
  this.env = opts.env ?? "prod";
370
383
  this.autoGuardrails = opts.autoGuardrails !== false;
384
+ const g = opts.autoGuardrailGroups ?? {};
385
+ this.autoGuardrailGroups = {
386
+ vitals: g.vitals ?? this.autoGuardrails,
387
+ errors: g.errors ?? this.autoGuardrails,
388
+ engagement: g.engagement ?? this.autoGuardrails
389
+ };
371
390
  this.anonId = getOrCreateAnonId();
372
391
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
373
392
  void this.buffer.flushPendingAlias();
@@ -396,9 +415,10 @@ var FlagsClientBrowser = class {
396
415
  const data = await res.json();
397
416
  if (seq !== this.identifySeq) return;
398
417
  this.evalResult = data;
399
- if (this.autoGuardrails && !this.guardrailsInstalled) {
418
+ const anyGroupOn = this.autoGuardrailGroups.vitals || this.autoGuardrailGroups.errors || this.autoGuardrailGroups.engagement;
419
+ if (anyGroupOn && !this.guardrailsInstalled) {
400
420
  this.guardrailsInstalled = true;
401
- installAutoGuardrails(this.buffer, this.userId, this.anonId);
421
+ installAutoGuardrails(this.buffer, this.userId, this.anonId, this.autoGuardrailGroups);
402
422
  }
403
423
  this.notify();
404
424
  }
@@ -614,9 +634,14 @@ function attachDevtools(client, opts = {}) {
614
634
  }
615
635
  var _client = null;
616
636
  function shipeasy(opts) {
637
+ const ac = opts.autoCollect;
638
+ const blanket = ac === false ? false : true;
639
+ const groups = ac && typeof ac === "object" ? ac : void 0;
617
640
  const client = configureShipeasy({
618
641
  sdkKey: opts.apiKey,
619
- baseUrl: opts.baseUrl ?? "https://cdn.shipeasy.ai"
642
+ baseUrl: opts.baseUrl ?? "https://cdn.shipeasy.ai",
643
+ autoGuardrails: blanket,
644
+ autoGuardrailGroups: groups
620
645
  });
621
646
  flags.notifyMounted();
622
647
  if (opts.autoIdentify !== false) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "2.1.15",
3
+ "version": "2.3.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",