@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.cjs CHANGED
@@ -8,7 +8,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
8
8
 
9
9
  // src/utils.ts
10
10
  var SDK_NAME = "sauron.javascript";
11
- var SDK_VERSION = "1.0.0";
11
+ var SDK_VERSION = "1.3.0";
12
12
  function getGlobal() {
13
13
  return globalThis;
14
14
  }
@@ -836,6 +836,30 @@ var Scope = class {
836
836
  }
837
837
  };
838
838
 
839
+ // src/workflow.ts
840
+ var WORKFLOW_NAME_MAX = 120;
841
+ var WORKFLOW_REASON_MAX = 120;
842
+ var current = null;
843
+ function getWorkflow() {
844
+ return current;
845
+ }
846
+ function setWorkflowState(workflow) {
847
+ current = workflow;
848
+ }
849
+ function resetWorkflow() {
850
+ current = null;
851
+ }
852
+ function normalizeWorkflowName(name) {
853
+ if (typeof name !== "string") return null;
854
+ const trimmed = name.trim();
855
+ if (trimmed.length === 0 || trimmed.length > WORKFLOW_NAME_MAX) return null;
856
+ return trimmed;
857
+ }
858
+ function normalizeReason(reason) {
859
+ if (typeof reason !== "string" || reason.trim().length === 0) return "user";
860
+ return reason.trim().slice(0, WORKFLOW_REASON_MAX);
861
+ }
862
+
839
863
  // src/api/product.ts
840
864
  function track(name, properties = {}, options = {}) {
841
865
  const client = getClient();
@@ -906,6 +930,86 @@ function trackTransaction(input) {
906
930
  const item = buildTransactionItem(input, client.getDistinctId(), getSessionId());
907
931
  client.captureItem(item);
908
932
  }
933
+ var NOOP_LOGGER = makeLogger(false);
934
+ function emitWorkflowClose(active, eventName, reason, logger) {
935
+ const properties = {
936
+ workflow_id: active.workflowId,
937
+ workflow_name: active.name,
938
+ duration_ms: Math.max(0, Date.now() - Date.parse(active.startedAt))
939
+ };
940
+ if (eventName === "$workflow_cancel") {
941
+ properties.reason = normalizeReason(reason);
942
+ }
943
+ try {
944
+ track(eventName, properties);
945
+ } catch (err) {
946
+ logger.warn(`${eventName}: failed to emit the lifecycle event`, err);
947
+ } finally {
948
+ resetWorkflow();
949
+ }
950
+ }
951
+ function startWorkflow(name, options) {
952
+ let logger = NOOP_LOGGER;
953
+ try {
954
+ const client = getClient();
955
+ if (!client || !client.isEnabled()) return { status: "disabled" };
956
+ logger = makeLogger(client.options.debug);
957
+ const normalized = normalizeWorkflowName(name);
958
+ if (!normalized) {
959
+ logger.warn("startWorkflow: invalid name", name);
960
+ return { status: "invalid_name" };
961
+ }
962
+ const active = getWorkflow();
963
+ if (active && !options?.force) {
964
+ logger.warn(
965
+ `startWorkflow("${normalized}"): "${active.name}" is already active; pass { force: true } to replace it`
966
+ );
967
+ return { status: "already_active" };
968
+ }
969
+ const workflow = {
970
+ workflowId: uuidv4(),
971
+ name: normalized,
972
+ startedAt: nowIso()
973
+ };
974
+ if (active) emitWorkflowClose(active, "$workflow_cancel", "superseded", logger);
975
+ setWorkflowState(workflow);
976
+ try {
977
+ track("$workflow_start", { workflow_id: workflow.workflowId, workflow_name: workflow.name });
978
+ } catch (err) {
979
+ logger.warn("startWorkflow: failed to emit $workflow_start", err);
980
+ }
981
+ return { status: "ok", workflowId: workflow.workflowId };
982
+ } catch (err) {
983
+ logger.warn("startWorkflow failed", err);
984
+ return { status: "disabled" };
985
+ }
986
+ }
987
+ function closeWorkflow(eventName, name, reason) {
988
+ let logger = NOOP_LOGGER;
989
+ try {
990
+ const client = getClient();
991
+ if (!client || !client.isEnabled()) return { status: "disabled" };
992
+ logger = makeLogger(client.options.debug);
993
+ const active = getWorkflow();
994
+ if (!active) return { status: "not_active" };
995
+ if (name !== void 0 && normalizeWorkflowName(name) !== active.name) {
996
+ logger.warn(`${eventName}: "${name}" does not match active workflow "${active.name}"`);
997
+ return { status: "name_mismatch" };
998
+ }
999
+ const workflowId = active.workflowId;
1000
+ emitWorkflowClose(active, eventName, reason, logger);
1001
+ return { status: "ok", workflowId };
1002
+ } catch (err) {
1003
+ logger.warn(`${eventName} failed`, err);
1004
+ return { status: "disabled" };
1005
+ }
1006
+ }
1007
+ function endWorkflow(name) {
1008
+ return closeWorkflow("$workflow_end", name);
1009
+ }
1010
+ function cancelWorkflow(name, options) {
1011
+ return closeWorkflow("$workflow_cancel", name, options?.reason);
1012
+ }
909
1013
 
910
1014
  // src/integrations/performance.ts
911
1015
  var PERF_FETCH = "__sauron_perf_fetch__";
@@ -1358,6 +1462,9 @@ var Transport = class {
1358
1462
  __publicField(this, "pending", []);
1359
1463
  __publicField(this, "timer", null);
1360
1464
  __publicField(this, "onlineHandler", null);
1465
+ /** Permanent auto-disable latch, flipped by this transport itself the moment
1466
+ * it classifies a response as 401/403 — it is the source of truth for
1467
+ * {@link isEnabled}, not merely a mirror of something the client decided. */
1361
1468
  __publicField(this, "disabled", false);
1362
1469
  this.dsn = config.dsn;
1363
1470
  this.makeEnvelope = config.makeEnvelope;
@@ -1405,6 +1512,10 @@ var Transport = class {
1405
1512
  this.pending = [];
1406
1513
  this.stop();
1407
1514
  }
1515
+ /** Whether the transport still accepts items (false once auth-disabled by a 401/403). */
1516
+ isEnabled() {
1517
+ return !this.disabled;
1518
+ }
1408
1519
  /** Queue an item for the next batch; flush eagerly once the batch is full. */
1409
1520
  send(item) {
1410
1521
  if (this.disabled) return;
@@ -1447,6 +1558,7 @@ var Transport = class {
1447
1558
  return;
1448
1559
  case "disable":
1449
1560
  this.logger.warn("server rejected credentials; disabling client");
1561
+ this.disable();
1450
1562
  this.onDisable();
1451
1563
  return;
1452
1564
  case "split": {
@@ -1484,6 +1596,7 @@ var Transport = class {
1484
1596
  outcome = { action: "retry_backoff" };
1485
1597
  }
1486
1598
  if (outcome.action === "disable") {
1599
+ this.disable();
1487
1600
  this.onDisable();
1488
1601
  this.offline.enqueue(json);
1489
1602
  return;
@@ -1649,8 +1762,19 @@ var SauronClient = class {
1649
1762
  getScope() {
1650
1763
  return this.scope;
1651
1764
  }
1765
+ /**
1766
+ * False once this client was explicitly disabled/closed, OR once the
1767
+ * transport has auto-disabled itself on a 401/403 (revoked/invalid DSN
1768
+ * key) — computed from the transport's own state on every call, not a
1769
+ * separately mirrored flag, so a propagation regression there cannot leave
1770
+ * this predicate stale. `this.transport` always exists once a client
1771
+ * exists (it is constructed synchronously in the constructor); the
1772
+ * "nothing installed yet" case is instead handled one layer up, by every
1773
+ * module-level API (`startWorkflow`, `track`, ...) treating `getClient() ===
1774
+ * null` as the no-op/disabled case before it ever reaches here.
1775
+ */
1652
1776
  isEnabled() {
1653
- return this.enabled;
1777
+ return this.enabled && this.transport.isEnabled();
1654
1778
  }
1655
1779
  /** The current distinct id: the user id when identified, else an anon id. */
1656
1780
  getDistinctId() {
@@ -1672,7 +1796,6 @@ var SauronClient = class {
1672
1796
  dsn: this.dsn.raw,
1673
1797
  sdk: { name: SDK_NAME, version: SDK_VERSION },
1674
1798
  sent_at: nowIso(),
1675
- environment: this.options.environment,
1676
1799
  release: this.options.release
1677
1800
  };
1678
1801
  const context = buildContext(this.options.release, this.scope.getUser());
@@ -1718,6 +1841,39 @@ var SauronClient = class {
1718
1841
  item.user = this.scope.getUser();
1719
1842
  }
1720
1843
  }
1844
+ /**
1845
+ * Stamp the active workflow (if any) onto a signal item.
1846
+ *
1847
+ * Done HERE — the single choke point every capture path funnels through —
1848
+ * rather than at each item-construction site, so a capture path added later
1849
+ * is stamped by construction instead of by remembering to. The keys are
1850
+ * ASSIGNED ONLY when a workflow is active: an item with no workflow keeps
1851
+ * them absent entirely (not present-as-`undefined`), which is what makes
1852
+ * `JSON.stringify` omit them and keeps the no-workflow wire bytes identical
1853
+ * to pre-1.3.0.
1854
+ *
1855
+ * Only error/event/transaction carry `workflow_id`/`workflow_name` columns
1856
+ * server-side — identify and breadcrumb_batch items are deliberately left
1857
+ * alone. An item that already carries an explicit `workflow_id` is left
1858
+ * untouched, matching how `enrichErrorItem` defers to caller-set fields.
1859
+ */
1860
+ stampWorkflow(item) {
1861
+ if (item.type !== "error" && item.type !== "event" && item.type !== "transaction") return;
1862
+ const hasId = item.workflow_id !== void 0;
1863
+ const hasName = item.workflow_name !== void 0;
1864
+ if (hasId || hasName) {
1865
+ if (hasId !== hasName) {
1866
+ this.logger.warn(
1867
+ "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."
1868
+ );
1869
+ }
1870
+ return;
1871
+ }
1872
+ const workflow = getWorkflow();
1873
+ if (!workflow) return;
1874
+ item.workflow_id = workflow.workflowId;
1875
+ item.workflow_name = workflow.name;
1876
+ }
1721
1877
  /**
1722
1878
  * Run an item through sampling (errors only) and `beforeSend`, then hand it to
1723
1879
  * the transport. Returns silently when dropped.
@@ -1731,6 +1887,7 @@ var SauronClient = class {
1731
1887
  }
1732
1888
  this.enrichErrorItem(item, hint);
1733
1889
  }
1890
+ this.stampWorkflow(item);
1734
1891
  let processed = item;
1735
1892
  if (this.options.beforeSend) {
1736
1893
  try {
@@ -1767,6 +1924,7 @@ var SauronClient = class {
1767
1924
  }
1768
1925
  onNavigation(null);
1769
1926
  resetScreen();
1927
+ resetWorkflow();
1770
1928
  unpatchAll();
1771
1929
  setDsnHost(null);
1772
1930
  this.installed = false;
@@ -1789,7 +1947,6 @@ function resolveOptions(options) {
1789
1947
  const t = options.transport ?? {};
1790
1948
  return {
1791
1949
  dsn: options.dsn,
1792
- environment: options.environment ?? "production",
1793
1950
  release: options.release ?? null,
1794
1951
  sampleRate: clamp(options.sampleRate ?? 1, 0, 1),
1795
1952
  maxBreadcrumbs: options.maxBreadcrumbs ?? 50,
@@ -1874,6 +2031,18 @@ function setScreen2(name) {
1874
2031
  function getScreen2() {
1875
2032
  return getScreen();
1876
2033
  }
2034
+ function startWorkflow2(name, options) {
2035
+ return startWorkflow(name, options);
2036
+ }
2037
+ function endWorkflow2(name) {
2038
+ return endWorkflow(name);
2039
+ }
2040
+ function cancelWorkflow2(name, options) {
2041
+ return cancelWorkflow(name, options);
2042
+ }
2043
+ function getWorkflow2() {
2044
+ return getWorkflow();
2045
+ }
1877
2046
  function addBreadcrumb2(breadcrumb, hint) {
1878
2047
  addBreadcrumb(breadcrumb, hint);
1879
2048
  }
@@ -1915,6 +2084,10 @@ var Sauron = {
1915
2084
  setExtra,
1916
2085
  setScreen: setScreen2,
1917
2086
  getScreen: getScreen2,
2087
+ startWorkflow: startWorkflow2,
2088
+ endWorkflow: endWorkflow2,
2089
+ cancelWorkflow: cancelWorkflow2,
2090
+ getWorkflow: getWorkflow2,
1918
2091
  flush,
1919
2092
  close,
1920
2093
  getClient
@@ -1928,13 +2101,16 @@ exports.Sauron = Sauron;
1928
2101
  exports.SauronClient = SauronClient;
1929
2102
  exports.addBreadcrumb = addBreadcrumb2;
1930
2103
  exports.buildEnvelope = buildEnvelope;
2104
+ exports.cancelWorkflow = cancelWorkflow2;
1931
2105
  exports.captureException = captureException2;
1932
2106
  exports.captureMessage = captureMessage2;
1933
2107
  exports.close = close;
1934
2108
  exports.default = index_default;
2109
+ exports.endWorkflow = endWorkflow2;
1935
2110
  exports.flush = flush;
1936
2111
  exports.getClient = getClient;
1937
2112
  exports.getScreen = getScreen2;
2113
+ exports.getWorkflow = getWorkflow2;
1938
2114
  exports.identify = identify2;
1939
2115
  exports.init = init2;
1940
2116
  exports.isInAppFrame = isInAppFrame;
@@ -1947,6 +2123,7 @@ exports.setScreen = setScreen2;
1947
2123
  exports.setTag = setTag;
1948
2124
  exports.setTags = setTags;
1949
2125
  exports.setUser = setUser;
2126
+ exports.startWorkflow = startWorkflow2;
1950
2127
  exports.track = track2;
1951
2128
  exports.trackTransaction = trackTransaction2;
1952
2129
  //# sourceMappingURL=index.cjs.map