@edraj/sauron-browser 1.0.0 → 1.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.
package/dist/index.d.cts CHANGED
@@ -74,6 +74,13 @@ interface ErrorItem {
74
74
  */
75
75
  user?: UserContext | null;
76
76
  session_id?: string | null;
77
+ /**
78
+ * The active workflow this item was stamped with, if any. A pair —
79
+ * always both present or both absent, never one without the other. See
80
+ * {@link ActiveWorkflow}.
81
+ */
82
+ workflow_id?: string;
83
+ workflow_name?: string;
77
84
  screen?: string | null;
78
85
  }
79
86
  /** A product-analytics event (PostHog-style `track`). */
@@ -82,6 +89,13 @@ interface EventItem {
82
89
  name: string;
83
90
  distinct_id: string | null;
84
91
  session_id?: string | null;
92
+ /**
93
+ * The active workflow this item was stamped with, if any. A pair —
94
+ * always both present or both absent, never one without the other. See
95
+ * {@link ActiveWorkflow}.
96
+ */
97
+ workflow_id?: string;
98
+ workflow_name?: string;
85
99
  screen?: string | null;
86
100
  timestamp: string;
87
101
  properties: Record<string, unknown>;
@@ -108,6 +122,13 @@ interface TransactionItem {
108
122
  url?: string | null;
109
123
  distinct_id?: string | null;
110
124
  session_id?: string | null;
125
+ /**
126
+ * The active workflow this item was stamped with, if any. A pair —
127
+ * always both present or both absent, never one without the other. See
128
+ * {@link ActiveWorkflow}.
129
+ */
130
+ workflow_id?: string;
131
+ workflow_name?: string;
111
132
  timestamp: string;
112
133
  }
113
134
  /** An identity association (PostHog-style `identify`). */
@@ -163,7 +184,6 @@ interface EnvelopeHeader {
163
184
  dsn: string;
164
185
  sdk: SdkInfo;
165
186
  sent_at: string;
166
- environment: string;
167
187
  release: string | null;
168
188
  }
169
189
  /** The complete, serializable envelope posted to the ingest gateway. */
@@ -191,6 +211,25 @@ interface CaptureOptions {
191
211
  interface TrackOptions extends CaptureOptions {
192
212
  screen?: string;
193
213
  }
214
+ /**
215
+ * Outcome of `startWorkflow` / `endWorkflow` / `cancelWorkflow`. Telemetry
216
+ * never throws — every call resolves to one of these instead.
217
+ */
218
+ type WorkflowStatus = 'ok' | 'already_active' | 'not_active' | 'name_mismatch' | 'invalid_name' | 'disabled';
219
+ /** Result of a workflow lifecycle call. */
220
+ interface WorkflowResult {
221
+ status: WorkflowStatus;
222
+ /** The workflow id involved, when `status` is `'ok'`. */
223
+ workflowId?: string;
224
+ }
225
+ /** The currently-active workflow, as returned by `getWorkflow()`. */
226
+ interface ActiveWorkflow {
227
+ /** Client-generated UUID — the server rollup key is `(app_id, workflow_id)`. */
228
+ workflowId: string;
229
+ name: string;
230
+ /** ISO-8601 UTC timestamp of `startWorkflow()`. */
231
+ startedAt: string;
232
+ }
194
233
  /** Value accepted by `setUser` — normalized into a `UserContext`. */
195
234
  type UserInput = (Partial<UserContext> & {
196
235
  id?: string | null;
@@ -211,7 +250,6 @@ interface TransportOptions {
211
250
  interface InitOptions {
212
251
  /** `https://<public_key>@<host>/<project_id>` */
213
252
  dsn: string;
214
- environment?: string;
215
253
  release?: string;
216
254
  /** Error sample rate in [0, 1]. Default 1 (send everything). */
217
255
  sampleRate?: number;
@@ -244,7 +282,6 @@ interface InitOptions {
244
282
  /** Fully-resolved options with all defaults applied. */
245
283
  interface ResolvedOptions {
246
284
  dsn: string;
247
- environment: string;
248
285
  release: string | null;
249
286
  sampleRate: number;
250
287
  maxBreadcrumbs: number;
@@ -361,6 +398,17 @@ declare class SauronClient {
361
398
  /** Install global handlers + auto-instrumentation and start the transport. */
362
399
  install(): void;
363
400
  getScope(): Scope;
401
+ /**
402
+ * False once this client was explicitly disabled/closed, OR once the
403
+ * transport has auto-disabled itself on a 401/403 (revoked/invalid DSN
404
+ * key) — computed from the transport's own state on every call, not a
405
+ * separately mirrored flag, so a propagation regression there cannot leave
406
+ * this predicate stale. `this.transport` always exists once a client
407
+ * exists (it is constructed synchronously in the constructor); the
408
+ * "nothing installed yet" case is instead handled one layer up, by every
409
+ * module-level API (`startWorkflow`, `track`, ...) treating `getClient() ===
410
+ * null` as the no-op/disabled case before it ever reaches here.
411
+ */
364
412
  isEnabled(): boolean;
365
413
  /** The current distinct id: the user id when identified, else an anon id. */
366
414
  getDistinctId(): string | null;
@@ -379,6 +427,23 @@ declare class SauronClient {
379
427
  * `event_id` is always minted so callers can correlate the report.
380
428
  */
381
429
  private enrichErrorItem;
430
+ /**
431
+ * Stamp the active workflow (if any) onto a signal item.
432
+ *
433
+ * Done HERE — the single choke point every capture path funnels through —
434
+ * rather than at each item-construction site, so a capture path added later
435
+ * is stamped by construction instead of by remembering to. The keys are
436
+ * ASSIGNED ONLY when a workflow is active: an item with no workflow keeps
437
+ * them absent entirely (not present-as-`undefined`), which is what makes
438
+ * `JSON.stringify` omit them and keeps the no-workflow wire bytes identical
439
+ * to pre-1.3.0.
440
+ *
441
+ * Only error/event/transaction carry `workflow_id`/`workflow_name` columns
442
+ * server-side — identify and breadcrumb_batch items are deliberately left
443
+ * alone. An item that already carries an explicit `workflow_id` is left
444
+ * untouched, matching how `enrichErrorItem` defers to caller-set fields.
445
+ */
446
+ private stampWorkflow;
382
447
  /**
383
448
  * Run an item through sampling (errors only) and `beforeSend`, then hand it to
384
449
  * the transport. Returns silently when dropped.
@@ -421,7 +486,7 @@ declare function parseError(err: unknown): Frame[];
421
486
  /** Small dependency-free helpers shared across the SDK. */
422
487
  /** SDK identity, embedded in every envelope header. */
423
488
  declare const SDK_NAME = "sauron.javascript";
424
- declare const SDK_VERSION = "1.0.0";
489
+ declare const SDK_VERSION = "1.3.0";
425
490
 
426
491
  /**
427
492
  * `@edraj/sauron-browser` — public API surface.
@@ -452,6 +517,23 @@ declare function trackTransaction(input: TransactionInput): void;
452
517
  declare function setScreen(name: string): void;
453
518
  /** The current screen name, or null. */
454
519
  declare function getScreen(): string | null;
520
+ /**
521
+ * Start a named, explicitly-bounded workflow. `workflow_id` is a fresh
522
+ * client-generated UUID; the id + name are then stamped on every subsequent
523
+ * event/error/transaction until the workflow ends or is cancelled. Optional —
524
+ * an app that never calls this behaves exactly as before.
525
+ */
526
+ declare function startWorkflow(name: string, options?: {
527
+ force?: boolean;
528
+ }): WorkflowResult;
529
+ /** End the active workflow (or the one named `name`, if given). */
530
+ declare function endWorkflow(name?: string): WorkflowResult;
531
+ /** Cancel the active workflow (or the one named `name`, if given). */
532
+ declare function cancelWorkflow(name?: string, options?: {
533
+ reason?: string;
534
+ }): WorkflowResult;
535
+ /** The currently active workflow, or `null` when none is active. */
536
+ declare function getWorkflow(): ActiveWorkflow | null;
455
537
  /** Record a breadcrumb. */
456
538
  declare function addBreadcrumb(breadcrumb: BreadcrumbInput, hint?: Hint): void;
457
539
  /** Set (or clear, with `null`) the current user. */
@@ -485,9 +567,13 @@ declare const Sauron: {
485
567
  setExtra: typeof setExtra;
486
568
  setScreen: typeof setScreen;
487
569
  getScreen: typeof getScreen;
570
+ startWorkflow: typeof startWorkflow;
571
+ endWorkflow: typeof endWorkflow;
572
+ cancelWorkflow: typeof cancelWorkflow;
573
+ getWorkflow: typeof getWorkflow;
488
574
  flush: typeof flush;
489
575
  close: typeof close;
490
576
  getClient: typeof getClient;
491
577
  };
492
578
 
493
- export { type AppContext, type BeforeBreadcrumb, type BeforeSend, type Breadcrumb, type BreadcrumbBatchItem, type BreadcrumbInput, type CaptureOptions, type Context, type DeviceContext, type Dsn, DsnError, type Envelope, type EnvelopeHeader, type EnvelopeItem, type ErrorItem, type EventItem, type ExceptionValue, type Frame, type Hint, type IdentifyItem, type InitOptions, type ItemType, type Level, type Mechanism, type OsContext, type ResolvedOptions, type RuntimeContext, SDK_NAME, SDK_VERSION, Sauron, SauronClient, type SdkInfo, type TrackOptions, type TransactionInput, type TransactionItem, type TransactionOp, type TransportOptions, type UserContext, type UserInput, addBreadcrumb, buildEnvelope, captureException, captureMessage, close, Sauron as default, flush, getClient, getScreen, identify, init, isInAppFrame, parseDsn, parseError, parseStackString, setContext, setExtra, setScreen, setTag, setTags, setUser, track, trackTransaction };
579
+ export { type ActiveWorkflow, type AppContext, type BeforeBreadcrumb, type BeforeSend, type Breadcrumb, type BreadcrumbBatchItem, type BreadcrumbInput, type CaptureOptions, type Context, type DeviceContext, type Dsn, DsnError, type Envelope, type EnvelopeHeader, type EnvelopeItem, type ErrorItem, type EventItem, type ExceptionValue, type Frame, type Hint, type IdentifyItem, type InitOptions, type ItemType, type Level, type Mechanism, type OsContext, type ResolvedOptions, type RuntimeContext, SDK_NAME, SDK_VERSION, Sauron, SauronClient, type SdkInfo, type TrackOptions, type TransactionInput, type TransactionItem, type TransactionOp, type TransportOptions, type UserContext, type UserInput, type WorkflowResult, type WorkflowStatus, addBreadcrumb, buildEnvelope, cancelWorkflow, captureException, captureMessage, close, Sauron as default, endWorkflow, flush, getClient, getScreen, getWorkflow, identify, init, isInAppFrame, parseDsn, parseError, parseStackString, setContext, setExtra, setScreen, setTag, setTags, setUser, startWorkflow, track, trackTransaction };
package/dist/index.d.ts CHANGED
@@ -74,6 +74,13 @@ interface ErrorItem {
74
74
  */
75
75
  user?: UserContext | null;
76
76
  session_id?: string | null;
77
+ /**
78
+ * The active workflow this item was stamped with, if any. A pair —
79
+ * always both present or both absent, never one without the other. See
80
+ * {@link ActiveWorkflow}.
81
+ */
82
+ workflow_id?: string;
83
+ workflow_name?: string;
77
84
  screen?: string | null;
78
85
  }
79
86
  /** A product-analytics event (PostHog-style `track`). */
@@ -82,6 +89,13 @@ interface EventItem {
82
89
  name: string;
83
90
  distinct_id: string | null;
84
91
  session_id?: string | null;
92
+ /**
93
+ * The active workflow this item was stamped with, if any. A pair —
94
+ * always both present or both absent, never one without the other. See
95
+ * {@link ActiveWorkflow}.
96
+ */
97
+ workflow_id?: string;
98
+ workflow_name?: string;
85
99
  screen?: string | null;
86
100
  timestamp: string;
87
101
  properties: Record<string, unknown>;
@@ -108,6 +122,13 @@ interface TransactionItem {
108
122
  url?: string | null;
109
123
  distinct_id?: string | null;
110
124
  session_id?: string | null;
125
+ /**
126
+ * The active workflow this item was stamped with, if any. A pair —
127
+ * always both present or both absent, never one without the other. See
128
+ * {@link ActiveWorkflow}.
129
+ */
130
+ workflow_id?: string;
131
+ workflow_name?: string;
111
132
  timestamp: string;
112
133
  }
113
134
  /** An identity association (PostHog-style `identify`). */
@@ -163,7 +184,6 @@ interface EnvelopeHeader {
163
184
  dsn: string;
164
185
  sdk: SdkInfo;
165
186
  sent_at: string;
166
- environment: string;
167
187
  release: string | null;
168
188
  }
169
189
  /** The complete, serializable envelope posted to the ingest gateway. */
@@ -191,6 +211,25 @@ interface CaptureOptions {
191
211
  interface TrackOptions extends CaptureOptions {
192
212
  screen?: string;
193
213
  }
214
+ /**
215
+ * Outcome of `startWorkflow` / `endWorkflow` / `cancelWorkflow`. Telemetry
216
+ * never throws — every call resolves to one of these instead.
217
+ */
218
+ type WorkflowStatus = 'ok' | 'already_active' | 'not_active' | 'name_mismatch' | 'invalid_name' | 'disabled';
219
+ /** Result of a workflow lifecycle call. */
220
+ interface WorkflowResult {
221
+ status: WorkflowStatus;
222
+ /** The workflow id involved, when `status` is `'ok'`. */
223
+ workflowId?: string;
224
+ }
225
+ /** The currently-active workflow, as returned by `getWorkflow()`. */
226
+ interface ActiveWorkflow {
227
+ /** Client-generated UUID — the server rollup key is `(app_id, workflow_id)`. */
228
+ workflowId: string;
229
+ name: string;
230
+ /** ISO-8601 UTC timestamp of `startWorkflow()`. */
231
+ startedAt: string;
232
+ }
194
233
  /** Value accepted by `setUser` — normalized into a `UserContext`. */
195
234
  type UserInput = (Partial<UserContext> & {
196
235
  id?: string | null;
@@ -211,7 +250,6 @@ interface TransportOptions {
211
250
  interface InitOptions {
212
251
  /** `https://<public_key>@<host>/<project_id>` */
213
252
  dsn: string;
214
- environment?: string;
215
253
  release?: string;
216
254
  /** Error sample rate in [0, 1]. Default 1 (send everything). */
217
255
  sampleRate?: number;
@@ -244,7 +282,6 @@ interface InitOptions {
244
282
  /** Fully-resolved options with all defaults applied. */
245
283
  interface ResolvedOptions {
246
284
  dsn: string;
247
- environment: string;
248
285
  release: string | null;
249
286
  sampleRate: number;
250
287
  maxBreadcrumbs: number;
@@ -361,6 +398,17 @@ declare class SauronClient {
361
398
  /** Install global handlers + auto-instrumentation and start the transport. */
362
399
  install(): void;
363
400
  getScope(): Scope;
401
+ /**
402
+ * False once this client was explicitly disabled/closed, OR once the
403
+ * transport has auto-disabled itself on a 401/403 (revoked/invalid DSN
404
+ * key) — computed from the transport's own state on every call, not a
405
+ * separately mirrored flag, so a propagation regression there cannot leave
406
+ * this predicate stale. `this.transport` always exists once a client
407
+ * exists (it is constructed synchronously in the constructor); the
408
+ * "nothing installed yet" case is instead handled one layer up, by every
409
+ * module-level API (`startWorkflow`, `track`, ...) treating `getClient() ===
410
+ * null` as the no-op/disabled case before it ever reaches here.
411
+ */
364
412
  isEnabled(): boolean;
365
413
  /** The current distinct id: the user id when identified, else an anon id. */
366
414
  getDistinctId(): string | null;
@@ -379,6 +427,23 @@ declare class SauronClient {
379
427
  * `event_id` is always minted so callers can correlate the report.
380
428
  */
381
429
  private enrichErrorItem;
430
+ /**
431
+ * Stamp the active workflow (if any) onto a signal item.
432
+ *
433
+ * Done HERE — the single choke point every capture path funnels through —
434
+ * rather than at each item-construction site, so a capture path added later
435
+ * is stamped by construction instead of by remembering to. The keys are
436
+ * ASSIGNED ONLY when a workflow is active: an item with no workflow keeps
437
+ * them absent entirely (not present-as-`undefined`), which is what makes
438
+ * `JSON.stringify` omit them and keeps the no-workflow wire bytes identical
439
+ * to pre-1.3.0.
440
+ *
441
+ * Only error/event/transaction carry `workflow_id`/`workflow_name` columns
442
+ * server-side — identify and breadcrumb_batch items are deliberately left
443
+ * alone. An item that already carries an explicit `workflow_id` is left
444
+ * untouched, matching how `enrichErrorItem` defers to caller-set fields.
445
+ */
446
+ private stampWorkflow;
382
447
  /**
383
448
  * Run an item through sampling (errors only) and `beforeSend`, then hand it to
384
449
  * the transport. Returns silently when dropped.
@@ -421,7 +486,7 @@ declare function parseError(err: unknown): Frame[];
421
486
  /** Small dependency-free helpers shared across the SDK. */
422
487
  /** SDK identity, embedded in every envelope header. */
423
488
  declare const SDK_NAME = "sauron.javascript";
424
- declare const SDK_VERSION = "1.0.0";
489
+ declare const SDK_VERSION = "1.3.0";
425
490
 
426
491
  /**
427
492
  * `@edraj/sauron-browser` — public API surface.
@@ -452,6 +517,23 @@ declare function trackTransaction(input: TransactionInput): void;
452
517
  declare function setScreen(name: string): void;
453
518
  /** The current screen name, or null. */
454
519
  declare function getScreen(): string | null;
520
+ /**
521
+ * Start a named, explicitly-bounded workflow. `workflow_id` is a fresh
522
+ * client-generated UUID; the id + name are then stamped on every subsequent
523
+ * event/error/transaction until the workflow ends or is cancelled. Optional —
524
+ * an app that never calls this behaves exactly as before.
525
+ */
526
+ declare function startWorkflow(name: string, options?: {
527
+ force?: boolean;
528
+ }): WorkflowResult;
529
+ /** End the active workflow (or the one named `name`, if given). */
530
+ declare function endWorkflow(name?: string): WorkflowResult;
531
+ /** Cancel the active workflow (or the one named `name`, if given). */
532
+ declare function cancelWorkflow(name?: string, options?: {
533
+ reason?: string;
534
+ }): WorkflowResult;
535
+ /** The currently active workflow, or `null` when none is active. */
536
+ declare function getWorkflow(): ActiveWorkflow | null;
455
537
  /** Record a breadcrumb. */
456
538
  declare function addBreadcrumb(breadcrumb: BreadcrumbInput, hint?: Hint): void;
457
539
  /** Set (or clear, with `null`) the current user. */
@@ -485,9 +567,13 @@ declare const Sauron: {
485
567
  setExtra: typeof setExtra;
486
568
  setScreen: typeof setScreen;
487
569
  getScreen: typeof getScreen;
570
+ startWorkflow: typeof startWorkflow;
571
+ endWorkflow: typeof endWorkflow;
572
+ cancelWorkflow: typeof cancelWorkflow;
573
+ getWorkflow: typeof getWorkflow;
488
574
  flush: typeof flush;
489
575
  close: typeof close;
490
576
  getClient: typeof getClient;
491
577
  };
492
578
 
493
- export { type AppContext, type BeforeBreadcrumb, type BeforeSend, type Breadcrumb, type BreadcrumbBatchItem, type BreadcrumbInput, type CaptureOptions, type Context, type DeviceContext, type Dsn, DsnError, type Envelope, type EnvelopeHeader, type EnvelopeItem, type ErrorItem, type EventItem, type ExceptionValue, type Frame, type Hint, type IdentifyItem, type InitOptions, type ItemType, type Level, type Mechanism, type OsContext, type ResolvedOptions, type RuntimeContext, SDK_NAME, SDK_VERSION, Sauron, SauronClient, type SdkInfo, type TrackOptions, type TransactionInput, type TransactionItem, type TransactionOp, type TransportOptions, type UserContext, type UserInput, addBreadcrumb, buildEnvelope, captureException, captureMessage, close, Sauron as default, flush, getClient, getScreen, identify, init, isInAppFrame, parseDsn, parseError, parseStackString, setContext, setExtra, setScreen, setTag, setTags, setUser, track, trackTransaction };
579
+ export { type ActiveWorkflow, type AppContext, type BeforeBreadcrumb, type BeforeSend, type Breadcrumb, type BreadcrumbBatchItem, type BreadcrumbInput, type CaptureOptions, type Context, type DeviceContext, type Dsn, DsnError, type Envelope, type EnvelopeHeader, type EnvelopeItem, type ErrorItem, type EventItem, type ExceptionValue, type Frame, type Hint, type IdentifyItem, type InitOptions, type ItemType, type Level, type Mechanism, type OsContext, type ResolvedOptions, type RuntimeContext, SDK_NAME, SDK_VERSION, Sauron, SauronClient, type SdkInfo, type TrackOptions, type TransactionInput, type TransactionItem, type TransactionOp, type TransportOptions, type UserContext, type UserInput, type WorkflowResult, type WorkflowStatus, addBreadcrumb, buildEnvelope, cancelWorkflow, captureException, captureMessage, close, Sauron as default, endWorkflow, flush, getClient, getScreen, getWorkflow, identify, init, isInAppFrame, parseDsn, parseError, parseStackString, setContext, setExtra, setScreen, setTag, setTags, setUser, startWorkflow, track, trackTransaction };
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4
4
 
5
5
  // src/utils.ts
6
6
  var SDK_NAME = "sauron.javascript";
7
- var SDK_VERSION = "1.0.0";
7
+ var SDK_VERSION = "1.3.0";
8
8
  function getGlobal() {
9
9
  return globalThis;
10
10
  }
@@ -832,6 +832,30 @@ var Scope = class {
832
832
  }
833
833
  };
834
834
 
835
+ // src/workflow.ts
836
+ var WORKFLOW_NAME_MAX = 120;
837
+ var WORKFLOW_REASON_MAX = 120;
838
+ var current = null;
839
+ function getWorkflow() {
840
+ return current;
841
+ }
842
+ function setWorkflowState(workflow) {
843
+ current = workflow;
844
+ }
845
+ function resetWorkflow() {
846
+ current = null;
847
+ }
848
+ function normalizeWorkflowName(name) {
849
+ if (typeof name !== "string") return null;
850
+ const trimmed = name.trim();
851
+ if (trimmed.length === 0 || trimmed.length > WORKFLOW_NAME_MAX) return null;
852
+ return trimmed;
853
+ }
854
+ function normalizeReason(reason) {
855
+ if (typeof reason !== "string" || reason.trim().length === 0) return "user";
856
+ return reason.trim().slice(0, WORKFLOW_REASON_MAX);
857
+ }
858
+
835
859
  // src/api/product.ts
836
860
  function track(name, properties = {}, options = {}) {
837
861
  const client = getClient();
@@ -902,6 +926,86 @@ function trackTransaction(input) {
902
926
  const item = buildTransactionItem(input, client.getDistinctId(), getSessionId());
903
927
  client.captureItem(item);
904
928
  }
929
+ var NOOP_LOGGER = makeLogger(false);
930
+ function emitWorkflowClose(active, eventName, reason, logger) {
931
+ const properties = {
932
+ workflow_id: active.workflowId,
933
+ workflow_name: active.name,
934
+ duration_ms: Math.max(0, Date.now() - Date.parse(active.startedAt))
935
+ };
936
+ if (eventName === "$workflow_cancel") {
937
+ properties.reason = normalizeReason(reason);
938
+ }
939
+ try {
940
+ track(eventName, properties);
941
+ } catch (err) {
942
+ logger.warn(`${eventName}: failed to emit the lifecycle event`, err);
943
+ } finally {
944
+ resetWorkflow();
945
+ }
946
+ }
947
+ function startWorkflow(name, options) {
948
+ let logger = NOOP_LOGGER;
949
+ try {
950
+ const client = getClient();
951
+ if (!client || !client.isEnabled()) return { status: "disabled" };
952
+ logger = makeLogger(client.options.debug);
953
+ const normalized = normalizeWorkflowName(name);
954
+ if (!normalized) {
955
+ logger.warn("startWorkflow: invalid name", name);
956
+ return { status: "invalid_name" };
957
+ }
958
+ const active = getWorkflow();
959
+ if (active && !options?.force) {
960
+ logger.warn(
961
+ `startWorkflow("${normalized}"): "${active.name}" is already active; pass { force: true } to replace it`
962
+ );
963
+ return { status: "already_active" };
964
+ }
965
+ const workflow = {
966
+ workflowId: uuidv4(),
967
+ name: normalized,
968
+ startedAt: nowIso()
969
+ };
970
+ if (active) emitWorkflowClose(active, "$workflow_cancel", "superseded", logger);
971
+ setWorkflowState(workflow);
972
+ try {
973
+ track("$workflow_start", { workflow_id: workflow.workflowId, workflow_name: workflow.name });
974
+ } catch (err) {
975
+ logger.warn("startWorkflow: failed to emit $workflow_start", err);
976
+ }
977
+ return { status: "ok", workflowId: workflow.workflowId };
978
+ } catch (err) {
979
+ logger.warn("startWorkflow failed", err);
980
+ return { status: "disabled" };
981
+ }
982
+ }
983
+ function closeWorkflow(eventName, name, reason) {
984
+ let logger = NOOP_LOGGER;
985
+ try {
986
+ const client = getClient();
987
+ if (!client || !client.isEnabled()) return { status: "disabled" };
988
+ logger = makeLogger(client.options.debug);
989
+ const active = getWorkflow();
990
+ if (!active) return { status: "not_active" };
991
+ if (name !== void 0 && normalizeWorkflowName(name) !== active.name) {
992
+ logger.warn(`${eventName}: "${name}" does not match active workflow "${active.name}"`);
993
+ return { status: "name_mismatch" };
994
+ }
995
+ const workflowId = active.workflowId;
996
+ emitWorkflowClose(active, eventName, reason, logger);
997
+ return { status: "ok", workflowId };
998
+ } catch (err) {
999
+ logger.warn(`${eventName} failed`, err);
1000
+ return { status: "disabled" };
1001
+ }
1002
+ }
1003
+ function endWorkflow(name) {
1004
+ return closeWorkflow("$workflow_end", name);
1005
+ }
1006
+ function cancelWorkflow(name, options) {
1007
+ return closeWorkflow("$workflow_cancel", name, options?.reason);
1008
+ }
905
1009
 
906
1010
  // src/integrations/performance.ts
907
1011
  var PERF_FETCH = "__sauron_perf_fetch__";
@@ -1354,6 +1458,9 @@ var Transport = class {
1354
1458
  __publicField(this, "pending", []);
1355
1459
  __publicField(this, "timer", null);
1356
1460
  __publicField(this, "onlineHandler", null);
1461
+ /** Permanent auto-disable latch, flipped by this transport itself the moment
1462
+ * it classifies a response as 401/403 — it is the source of truth for
1463
+ * {@link isEnabled}, not merely a mirror of something the client decided. */
1357
1464
  __publicField(this, "disabled", false);
1358
1465
  this.dsn = config.dsn;
1359
1466
  this.makeEnvelope = config.makeEnvelope;
@@ -1401,6 +1508,10 @@ var Transport = class {
1401
1508
  this.pending = [];
1402
1509
  this.stop();
1403
1510
  }
1511
+ /** Whether the transport still accepts items (false once auth-disabled by a 401/403). */
1512
+ isEnabled() {
1513
+ return !this.disabled;
1514
+ }
1404
1515
  /** Queue an item for the next batch; flush eagerly once the batch is full. */
1405
1516
  send(item) {
1406
1517
  if (this.disabled) return;
@@ -1443,6 +1554,7 @@ var Transport = class {
1443
1554
  return;
1444
1555
  case "disable":
1445
1556
  this.logger.warn("server rejected credentials; disabling client");
1557
+ this.disable();
1446
1558
  this.onDisable();
1447
1559
  return;
1448
1560
  case "split": {
@@ -1480,6 +1592,7 @@ var Transport = class {
1480
1592
  outcome = { action: "retry_backoff" };
1481
1593
  }
1482
1594
  if (outcome.action === "disable") {
1595
+ this.disable();
1483
1596
  this.onDisable();
1484
1597
  this.offline.enqueue(json);
1485
1598
  return;
@@ -1645,8 +1758,19 @@ var SauronClient = class {
1645
1758
  getScope() {
1646
1759
  return this.scope;
1647
1760
  }
1761
+ /**
1762
+ * False once this client was explicitly disabled/closed, OR once the
1763
+ * transport has auto-disabled itself on a 401/403 (revoked/invalid DSN
1764
+ * key) — computed from the transport's own state on every call, not a
1765
+ * separately mirrored flag, so a propagation regression there cannot leave
1766
+ * this predicate stale. `this.transport` always exists once a client
1767
+ * exists (it is constructed synchronously in the constructor); the
1768
+ * "nothing installed yet" case is instead handled one layer up, by every
1769
+ * module-level API (`startWorkflow`, `track`, ...) treating `getClient() ===
1770
+ * null` as the no-op/disabled case before it ever reaches here.
1771
+ */
1648
1772
  isEnabled() {
1649
- return this.enabled;
1773
+ return this.enabled && this.transport.isEnabled();
1650
1774
  }
1651
1775
  /** The current distinct id: the user id when identified, else an anon id. */
1652
1776
  getDistinctId() {
@@ -1668,7 +1792,6 @@ var SauronClient = class {
1668
1792
  dsn: this.dsn.raw,
1669
1793
  sdk: { name: SDK_NAME, version: SDK_VERSION },
1670
1794
  sent_at: nowIso(),
1671
- environment: this.options.environment,
1672
1795
  release: this.options.release
1673
1796
  };
1674
1797
  const context = buildContext(this.options.release, this.scope.getUser());
@@ -1714,6 +1837,39 @@ var SauronClient = class {
1714
1837
  item.user = this.scope.getUser();
1715
1838
  }
1716
1839
  }
1840
+ /**
1841
+ * Stamp the active workflow (if any) onto a signal item.
1842
+ *
1843
+ * Done HERE — the single choke point every capture path funnels through —
1844
+ * rather than at each item-construction site, so a capture path added later
1845
+ * is stamped by construction instead of by remembering to. The keys are
1846
+ * ASSIGNED ONLY when a workflow is active: an item with no workflow keeps
1847
+ * them absent entirely (not present-as-`undefined`), which is what makes
1848
+ * `JSON.stringify` omit them and keeps the no-workflow wire bytes identical
1849
+ * to pre-1.3.0.
1850
+ *
1851
+ * Only error/event/transaction carry `workflow_id`/`workflow_name` columns
1852
+ * server-side — identify and breadcrumb_batch items are deliberately left
1853
+ * alone. An item that already carries an explicit `workflow_id` is left
1854
+ * untouched, matching how `enrichErrorItem` defers to caller-set fields.
1855
+ */
1856
+ stampWorkflow(item) {
1857
+ if (item.type !== "error" && item.type !== "event" && item.type !== "transaction") return;
1858
+ const hasId = item.workflow_id !== void 0;
1859
+ const hasName = item.workflow_name !== void 0;
1860
+ if (hasId || hasName) {
1861
+ if (hasId !== hasName) {
1862
+ this.logger.warn(
1863
+ "item sets only one of workflow_id/workflow_name; the server treats them as a pair and will drop this attribution. Set both, or neither."
1864
+ );
1865
+ }
1866
+ return;
1867
+ }
1868
+ const workflow = getWorkflow();
1869
+ if (!workflow) return;
1870
+ item.workflow_id = workflow.workflowId;
1871
+ item.workflow_name = workflow.name;
1872
+ }
1717
1873
  /**
1718
1874
  * Run an item through sampling (errors only) and `beforeSend`, then hand it to
1719
1875
  * the transport. Returns silently when dropped.
@@ -1727,6 +1883,7 @@ var SauronClient = class {
1727
1883
  }
1728
1884
  this.enrichErrorItem(item, hint);
1729
1885
  }
1886
+ this.stampWorkflow(item);
1730
1887
  let processed = item;
1731
1888
  if (this.options.beforeSend) {
1732
1889
  try {
@@ -1763,6 +1920,7 @@ var SauronClient = class {
1763
1920
  }
1764
1921
  onNavigation(null);
1765
1922
  resetScreen();
1923
+ resetWorkflow();
1766
1924
  unpatchAll();
1767
1925
  setDsnHost(null);
1768
1926
  this.installed = false;
@@ -1785,7 +1943,6 @@ function resolveOptions(options) {
1785
1943
  const t = options.transport ?? {};
1786
1944
  return {
1787
1945
  dsn: options.dsn,
1788
- environment: options.environment ?? "production",
1789
1946
  release: options.release ?? null,
1790
1947
  sampleRate: clamp(options.sampleRate ?? 1, 0, 1),
1791
1948
  maxBreadcrumbs: options.maxBreadcrumbs ?? 50,
@@ -1870,6 +2027,18 @@ function setScreen2(name) {
1870
2027
  function getScreen2() {
1871
2028
  return getScreen();
1872
2029
  }
2030
+ function startWorkflow2(name, options) {
2031
+ return startWorkflow(name, options);
2032
+ }
2033
+ function endWorkflow2(name) {
2034
+ return endWorkflow(name);
2035
+ }
2036
+ function cancelWorkflow2(name, options) {
2037
+ return cancelWorkflow(name, options);
2038
+ }
2039
+ function getWorkflow2() {
2040
+ return getWorkflow();
2041
+ }
1873
2042
  function addBreadcrumb2(breadcrumb, hint) {
1874
2043
  addBreadcrumb(breadcrumb, hint);
1875
2044
  }
@@ -1911,12 +2080,16 @@ var Sauron = {
1911
2080
  setExtra,
1912
2081
  setScreen: setScreen2,
1913
2082
  getScreen: getScreen2,
2083
+ startWorkflow: startWorkflow2,
2084
+ endWorkflow: endWorkflow2,
2085
+ cancelWorkflow: cancelWorkflow2,
2086
+ getWorkflow: getWorkflow2,
1914
2087
  flush,
1915
2088
  close,
1916
2089
  getClient
1917
2090
  };
1918
2091
  var index_default = Sauron;
1919
2092
 
1920
- export { DsnError, SDK_NAME, SDK_VERSION, Sauron, SauronClient, addBreadcrumb2 as addBreadcrumb, buildEnvelope, captureException2 as captureException, captureMessage2 as captureMessage, close, index_default as default, flush, getClient, getScreen2 as getScreen, identify2 as identify, init2 as init, isInAppFrame, parseDsn, parseError, parseStackString, setContext, setExtra, setScreen2 as setScreen, setTag, setTags, setUser, track2 as track, trackTransaction2 as trackTransaction };
2093
+ export { DsnError, SDK_NAME, SDK_VERSION, Sauron, SauronClient, addBreadcrumb2 as addBreadcrumb, buildEnvelope, cancelWorkflow2 as cancelWorkflow, captureException2 as captureException, captureMessage2 as captureMessage, close, index_default as default, endWorkflow2 as endWorkflow, flush, getClient, getScreen2 as getScreen, getWorkflow2 as getWorkflow, identify2 as identify, init2 as init, isInAppFrame, parseDsn, parseError, parseStackString, setContext, setExtra, setScreen2 as setScreen, setTag, setTags, setUser, startWorkflow2 as startWorkflow, track2 as track, trackTransaction2 as trackTransaction };
1921
2094
  //# sourceMappingURL=index.js.map
1922
2095
  //# sourceMappingURL=index.js.map