@allstak/react-native 0.3.1 → 0.3.2

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.js CHANGED
@@ -30,11 +30,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ ALWAYS_REDACT_HEADERS: () => ALWAYS_REDACT_HEADERS,
34
+ ALWAYS_REDACT_QUERY: () => ALWAYS_REDACT_QUERY,
33
35
  AllStak: () => AllStak,
34
36
  AllStakClient: () => AllStakClient,
35
37
  AllStakProvider: () => AllStakProvider,
38
+ DEFAULT_REDACT_BODY_FIELDS: () => DEFAULT_REDACT_BODY_FIELDS,
36
39
  HttpRequestModule: () => HttpRequestModule,
37
40
  INGEST_HOST: () => INGEST_HOST,
41
+ REDACTED: () => REDACTED,
38
42
  ReplaySurrogate: () => ReplaySurrogate,
39
43
  SDK_NAME: () => SDK_NAME,
40
44
  SDK_VERSION: () => SDK_VERSION,
@@ -45,39 +49,71 @@ __export(index_exports, {
45
49
  __resetProviderInstanceForTest: () => __resetProviderInstanceForTest,
46
50
  __setNativeModuleForTest: () => __setNativeModuleForTest,
47
51
  applyArchitectureTags: () => applyArchitectureTags,
52
+ captureBodyResult: () => captureBodyResult,
48
53
  detectArchitecture: () => detectArchitecture,
49
54
  drainPendingNativeCrashes: () => drainPendingNativeCrashes,
50
55
  installReactNative: () => installReactNative,
51
56
  instrumentNavigationFromLinking: () => instrumentNavigationFromLinking,
52
57
  instrumentReactNavigation: () => instrumentReactNavigation,
58
+ redactUrl: () => redactUrl,
59
+ sanitizeHeaders: () => sanitizeHeaders,
53
60
  tryAutoInstrumentNavigation: () => tryAutoInstrumentNavigation,
54
61
  useAllStak: () => useAllStak
55
62
  });
56
63
  module.exports = __toCommonJS(index_exports);
57
64
 
58
65
  // src/transport.ts
59
- var REQUEST_TIMEOUT = 3e3;
66
+ var REQUEST_TIMEOUT = 2e3;
60
67
  var MAX_BUFFER = 100;
68
+ var FAILURE_THRESHOLD = 3;
69
+ var BACKOFF_BASE_MS = 500;
70
+ var BACKOFF_MAX_MS = 3e4;
61
71
  var HttpTransport = class {
62
- constructor(baseUrl, apiKey) {
72
+ constructor(baseUrl, apiKey, enabled = true) {
63
73
  this.baseUrl = baseUrl;
64
74
  this.apiKey = apiKey;
75
+ this.enabled = enabled;
65
76
  this.buffer = [];
66
77
  this.flushing = false;
78
+ this.consecutiveFailures = 0;
79
+ this.circuitOpenUntil = 0;
80
+ this.sent = 0;
81
+ this.failed = 0;
82
+ this.dropped = 0;
83
+ }
84
+ send(path, payload) {
85
+ if (!this.enabled) {
86
+ this.noteDropped();
87
+ return Promise.resolve();
88
+ }
89
+ this.enqueueOrDispatch({ path, payload });
90
+ return Promise.resolve();
91
+ }
92
+ enqueueOrDispatch(item) {
93
+ if (Date.now() < this.circuitOpenUntil) {
94
+ this.push(item);
95
+ return;
96
+ }
97
+ void this.dispatch(item).catch(() => void 0);
67
98
  }
68
- async send(path, payload) {
99
+ async dispatch(item) {
69
100
  try {
70
- await this.doFetch(path, payload);
71
- await this.flushBuffer();
72
- } catch {
73
- if (this.buffer.length >= MAX_BUFFER) this.buffer.shift();
74
- this.buffer.push({ path, payload });
101
+ await this.doFetch(item.path, item.payload);
102
+ this.sent++;
103
+ this.consecutiveFailures = 0;
104
+ this.circuitOpenUntil = 0;
105
+ this.scheduleFlush();
106
+ } catch (err) {
107
+ this.failed++;
108
+ this.recordFailure(err);
109
+ this.push(item);
75
110
  }
76
111
  }
77
112
  async doFetch(path, payload) {
78
113
  const url = `${this.baseUrl}${path}`;
79
114
  const controller = new AbortController();
80
115
  const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
116
+ const started = Date.now();
81
117
  try {
82
118
  const res = await fetch(url, {
83
119
  method: "POST",
@@ -91,26 +127,77 @@ var HttpTransport = class {
91
127
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
92
128
  } finally {
93
129
  clearTimeout(timeoutId);
130
+ this.lastTransportLatencyMs = Date.now() - started;
131
+ }
132
+ }
133
+ push(item) {
134
+ if (this.buffer.length >= MAX_BUFFER) {
135
+ this.buffer.shift();
136
+ this.dropped++;
94
137
  }
138
+ this.buffer.push(item);
139
+ }
140
+ scheduleFlush() {
141
+ if (this.flushing || this.buffer.length === 0) return;
142
+ const delay = Math.max(0, this.circuitOpenUntil - Date.now());
143
+ const timer = setTimeout(() => {
144
+ void this.flushBuffer().catch(() => void 0);
145
+ }, delay);
146
+ if (typeof timer === "object" && typeof timer.unref === "function") timer.unref();
95
147
  }
96
148
  async flushBuffer() {
97
149
  if (this.flushing || this.buffer.length === 0) return;
98
150
  this.flushing = true;
151
+ const started = Date.now();
99
152
  try {
100
153
  const items = this.buffer.splice(0, this.buffer.length);
101
154
  for (const item of items) {
155
+ if (Date.now() < this.circuitOpenUntil) {
156
+ this.push(item);
157
+ continue;
158
+ }
102
159
  try {
103
160
  await this.doFetch(item.path, item.payload);
104
- } catch {
161
+ this.sent++;
162
+ this.consecutiveFailures = 0;
163
+ this.circuitOpenUntil = 0;
164
+ } catch (err) {
165
+ this.failed++;
166
+ this.recordFailure(err);
167
+ this.push(item);
105
168
  }
106
169
  }
107
170
  } finally {
171
+ this.lastFlushDurationMs = Date.now() - started;
108
172
  this.flushing = false;
173
+ if (this.buffer.length > 0) this.scheduleFlush();
109
174
  }
110
175
  }
176
+ recordFailure(error) {
177
+ this.consecutiveFailures++;
178
+ if (this.consecutiveFailures < FAILURE_THRESHOLD) return;
179
+ const retryAfterMs = retryAfterFromError(error);
180
+ const backoff = retryAfterMs ?? jitteredBackoff(this.consecutiveFailures);
181
+ this.circuitOpenUntil = Date.now() + backoff;
182
+ }
111
183
  getBufferSize() {
112
184
  return this.buffer.length;
113
185
  }
186
+ noteDropped(count = 1) {
187
+ this.dropped += Math.max(0, count);
188
+ }
189
+ getStats() {
190
+ return {
191
+ queued: this.buffer.length,
192
+ sent: this.sent,
193
+ failed: this.failed,
194
+ dropped: this.dropped,
195
+ consecutiveFailures: this.consecutiveFailures,
196
+ circuitOpenUntil: this.circuitOpenUntil,
197
+ lastTransportLatencyMs: this.lastTransportLatencyMs,
198
+ lastFlushDurationMs: this.lastFlushDurationMs
199
+ };
200
+ }
114
201
  /**
115
202
  * Wait for the in-flight retry-buffer to drain. Resolves `true` if the
116
203
  * buffer empties within `timeoutMs` (default 2000ms), `false` otherwise.
@@ -127,6 +214,14 @@ var HttpTransport = class {
127
214
  return true;
128
215
  }
129
216
  };
217
+ function jitteredBackoff(failures) {
218
+ const exp = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** Math.min(8, failures - FAILURE_THRESHOLD));
219
+ return Math.floor(exp / 2 + Math.random() * (exp / 2));
220
+ }
221
+ function retryAfterFromError(error) {
222
+ const message = error instanceof Error ? error.message : "";
223
+ return /HTTP\s+(429|503)/.test(message) ? BACKOFF_MAX_MS : null;
224
+ }
130
225
 
131
226
  // src/stack.ts
132
227
  var V8_FRAME_RE = /^\s*at\s+(?:(.+?)\s+\()?((?:.+?):(\d+):(\d+))\)?\s*$/;
@@ -181,6 +276,24 @@ function isInApp(filename) {
181
276
  return true;
182
277
  }
183
278
 
279
+ // src/utils/debug-id.ts
280
+ var REGISTRY_KEY = "_allstakDebugIds";
281
+ var cache = /* @__PURE__ */ new Map();
282
+ function resolveDebugId(filename) {
283
+ if (!filename) return void 0;
284
+ if (cache.has(filename)) return cache.get(filename) ?? void 0;
285
+ const registry = globalThis[REGISTRY_KEY];
286
+ if (registry && typeof registry === "object") {
287
+ const hit = registry[filename];
288
+ if (typeof hit === "string" && hit.length > 0) {
289
+ cache.set(filename, hit);
290
+ return hit;
291
+ }
292
+ }
293
+ cache.set(filename, null);
294
+ return void 0;
295
+ }
296
+
184
297
  // src/scope.ts
185
298
  var Scope = class {
186
299
  constructor() {
@@ -392,6 +505,7 @@ var TracingModule = class {
392
505
  }
393
506
  if (!this.flushTimer) {
394
507
  this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
508
+ this.flushTimer?.unref?.();
395
509
  }
396
510
  }
397
511
  flush() {
@@ -437,10 +551,11 @@ var ReplaySurrogate = class {
437
551
  if (Math.random() >= this.opts.sampleRate) return false;
438
552
  this.active = true;
439
553
  this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS2);
554
+ this.flushTimer?.unref?.();
440
555
  return true;
441
556
  }
442
557
  /** Record a screen view. Filters params through the safeParams allow-list. */
443
- recordScreenView(routeName, params) {
558
+ recordScreenView(routeName, params, context) {
444
559
  if (!this.active) return;
445
560
  const safe = {};
446
561
  if (params && this.opts.safeParams.length > 0) {
@@ -448,17 +563,22 @@ var ReplaySurrogate = class {
448
563
  if (key in params) safe[key] = params[key];
449
564
  }
450
565
  }
451
- this.push({ ts: Date.now(), k: "screen", data: { route: routeName, params: safe } });
566
+ this.push({ ts: Date.now(), k: "screen", data: { route: routeName, params: safe, ...compact(context) } });
452
567
  }
453
568
  /** Record an AppState transition (foreground/background/inactive). */
454
- recordAppState(next) {
569
+ recordAppState(next, context) {
455
570
  if (!this.active) return;
456
- this.push({ ts: Date.now(), k: "appstate", data: { state: next } });
571
+ this.push({ ts: Date.now(), k: "appstate", data: { state: next, ...compact(context) } });
457
572
  }
458
573
  /** Record a free-form, customer-validated checkpoint. */
459
- recordManual(label, data) {
574
+ recordManual(label, data, context) {
575
+ if (!this.active) return;
576
+ this.push({ ts: Date.now(), k: "manual", data: { label, ...data ?? {}, ...compact(context) } });
577
+ }
578
+ /** Record a forensic mobile session timeline marker. This is not replay. */
579
+ recordTimelineMarker(kind, label, data, context) {
460
580
  if (!this.active) return;
461
- this.push({ ts: Date.now(), k: "manual", data: { label, ...data ?? {} } });
581
+ this.push({ ts: Date.now(), k: kind, data: { label, ...data ?? {}, ...compact(context) } });
462
582
  }
463
583
  destroy() {
464
584
  this.destroyed = true;
@@ -492,6 +612,14 @@ var ReplaySurrogate = class {
492
612
  });
493
613
  }
494
614
  };
615
+ function compact(context) {
616
+ if (!context) return {};
617
+ const out = {};
618
+ for (const [key, value] of Object.entries(context)) {
619
+ if (value !== void 0 && value !== null && value !== "") out[key] = value;
620
+ }
621
+ return out;
622
+ }
495
623
 
496
624
  // src/http-requests.ts
497
625
  var INGEST_PATH = "/ingest/v1/http-requests";
@@ -503,6 +631,12 @@ function genTraceId() {
503
631
  const seg = (len) => Array.from({ length: len }, () => hex(16)).join("");
504
632
  return `${seg(8)}-${seg(4)}-4${seg(3)}-${(8 + Math.floor(Math.random() * 4)).toString(16)}${seg(3)}-${seg(12)}`;
505
633
  }
634
+ function genRequestId() {
635
+ return genTraceId();
636
+ }
637
+ function generateHttpId() {
638
+ return genTraceId();
639
+ }
506
640
  function splitHostPath(url) {
507
641
  try {
508
642
  const u = new URL(url);
@@ -529,6 +663,7 @@ var HttpRequestModule = class {
529
663
  const item = {
530
664
  type: "http_request",
531
665
  traceId: ev.traceId ?? genTraceId(),
666
+ requestId: ev.requestId ?? genRequestId(),
532
667
  direction: "outbound",
533
668
  method: (ev.method || "GET").toUpperCase(),
534
669
  host,
@@ -543,6 +678,16 @@ var HttpRequestModule = class {
543
678
  requestHeaders: ev.requestHeaders,
544
679
  responseHeaders: ev.responseHeaders,
545
680
  error: ev.error,
681
+ spanId: ev.spanId,
682
+ parentSpanId: ev.parentSpanId,
683
+ requestBodyStatus: ev.requestBodyStatus,
684
+ responseBodyStatus: ev.responseBodyStatus,
685
+ requestBodyRedactedFields: ev.requestBodyRedactedFields,
686
+ responseBodyRedactedFields: ev.responseBodyRedactedFields,
687
+ requestBodyTruncated: ev.requestBodyTruncated,
688
+ responseBodyTruncated: ev.responseBodyTruncated,
689
+ capturePolicy: ev.capturePolicy,
690
+ linkConfidence: ev.linkConfidence ?? "exact",
546
691
  environment: this.defaults.environment,
547
692
  release: this.defaults.release,
548
693
  dist: this.defaults.dist,
@@ -561,7 +706,10 @@ var HttpRequestModule = class {
561
706
  this.flush();
562
707
  return;
563
708
  }
564
- if (!this.flushTimer) this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS3);
709
+ if (!this.flushTimer) {
710
+ this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS3);
711
+ this.flushTimer?.unref?.();
712
+ }
565
713
  }
566
714
  /** Snapshot of the last failed requests for error-linking. Newest last. */
567
715
  getRecentFailed() {
@@ -613,6 +761,24 @@ var ALWAYS_REDACT_QUERY = /* @__PURE__ */ new Set([
613
761
  "jwt"
614
762
  ]);
615
763
  var REDACTED = "[REDACTED]";
764
+ var DEFAULT_REDACT_BODY_FIELDS = [
765
+ "password",
766
+ "passcode",
767
+ "otp",
768
+ "token",
769
+ "authorization",
770
+ "cookie",
771
+ "session",
772
+ "refresh_token",
773
+ "access_token",
774
+ "jwt",
775
+ "card",
776
+ "credit_card",
777
+ "iban",
778
+ "national_id",
779
+ "secret",
780
+ "api_key"
781
+ ];
616
782
  function shouldCaptureUrl(url, opts) {
617
783
  if (!url) return false;
618
784
  const lower = url.toLowerCase();
@@ -680,25 +846,125 @@ function sanitizeHeaders(headers, opts) {
680
846
  }
681
847
  return out;
682
848
  }
683
- function captureBody(body, enabled, maxBodyBytes) {
684
- if (!enabled) return void 0;
685
- if (body == null) return void 0;
849
+ function captureBodyResult(body, enabled, maxBodyBytes, opts = {}, contentType) {
850
+ if (!enabled) {
851
+ return {
852
+ status: "disabled",
853
+ redactedFields: [],
854
+ truncated: false,
855
+ capturePolicy: "body_capture_disabled"
856
+ };
857
+ }
858
+ if (body == null) {
859
+ return {
860
+ status: "empty",
861
+ redactedFields: [],
862
+ truncated: false,
863
+ capturePolicy: "empty_body"
864
+ };
865
+ }
866
+ const allowed = opts.allowedContentTypes ?? ["application/json", "text/", "application/problem+json"];
867
+ if (contentType && !allowed.some((needle) => contentType.toLowerCase().includes(needle.toLowerCase()))) {
868
+ return {
869
+ status: "unsupported",
870
+ redactedFields: [],
871
+ truncated: false,
872
+ capturePolicy: `unsupported_content_type:${contentType}`
873
+ };
874
+ }
686
875
  let str;
876
+ let redactedFields = [];
687
877
  if (typeof body === "string") str = body;
688
878
  else if (typeof body === "number" || typeof body === "boolean") str = String(body);
689
879
  else if (typeof body === "object") {
690
880
  const tag = Object.prototype.toString.call(body);
691
- if (tag !== "[object Object]" && tag !== "[object Array]") return "<binary>";
881
+ if (tag !== "[object Object]" && tag !== "[object Array]") {
882
+ return {
883
+ body: "<binary>",
884
+ status: "unsupported",
885
+ redactedFields: [],
886
+ truncated: false,
887
+ capturePolicy: "unsupported_binary_body"
888
+ };
889
+ }
890
+ const redacted = redactJsonValue(body, opts);
891
+ redactedFields = redacted.redactedFields;
692
892
  try {
693
- str = JSON.stringify(body);
893
+ str = JSON.stringify(redacted.value);
694
894
  } catch {
695
- return "<unserializable>";
895
+ return {
896
+ body: "<unserializable>",
897
+ status: "unsupported",
898
+ redactedFields,
899
+ truncated: false,
900
+ capturePolicy: "unserializable_body"
901
+ };
696
902
  }
697
903
  } else {
698
- return "<binary>";
904
+ return {
905
+ body: "<binary>",
906
+ status: "unsupported",
907
+ redactedFields: [],
908
+ truncated: false,
909
+ capturePolicy: "unsupported_body_type"
910
+ };
699
911
  }
700
- if (str.length > maxBodyBytes) return str.slice(0, maxBodyBytes) + "\u2026[truncated]";
701
- return str;
912
+ if (typeof body === "string" && looksJson(contentType, str)) {
913
+ try {
914
+ const redacted = redactJsonValue(JSON.parse(str), opts);
915
+ redactedFields = redacted.redactedFields;
916
+ str = JSON.stringify(redacted.value);
917
+ } catch {
918
+ str = redactSensitiveText(str, opts);
919
+ }
920
+ }
921
+ let truncated = false;
922
+ if (str.length > maxBodyBytes) {
923
+ str = str.slice(0, maxBodyBytes) + "\u2026[truncated]";
924
+ truncated = true;
925
+ }
926
+ return {
927
+ body: str,
928
+ status: truncated ? "truncated" : redactedFields.length > 0 ? "redacted" : "captured",
929
+ redactedFields: Array.from(new Set(redactedFields)).sort(),
930
+ truncated,
931
+ capturePolicy: "opt_in_body_capture"
932
+ };
933
+ }
934
+ function looksJson(contentType, body) {
935
+ if (contentType && contentType.toLowerCase().includes("json")) return true;
936
+ const trimmed = body.trim();
937
+ return trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]");
938
+ }
939
+ function redactJsonValue(value, opts, path = "") {
940
+ const fieldSet = new Set([...DEFAULT_REDACT_BODY_FIELDS, ...opts.redactBodyFields ?? []].map((v) => v.toLowerCase()));
941
+ const redactedFields = [];
942
+ const walk = (input, currentPath) => {
943
+ if (Array.isArray(input)) return input.map((item, index) => walk(item, `${currentPath}[${index}]`));
944
+ if (!input || typeof input !== "object") return input;
945
+ const out = {};
946
+ for (const [key, raw] of Object.entries(input)) {
947
+ const keyLower = key.toLowerCase();
948
+ const nextPath = currentPath ? `${currentPath}.${key}` : key;
949
+ if (fieldSet.has(keyLower) || keyLower.includes("token") || keyLower.includes("password")) {
950
+ out[key] = REDACTED;
951
+ redactedFields.push(nextPath);
952
+ } else {
953
+ out[key] = walk(raw, nextPath);
954
+ }
955
+ }
956
+ return out;
957
+ };
958
+ return { value: walk(value, path), redactedFields };
959
+ }
960
+ function redactSensitiveText(input, opts) {
961
+ const fields = [...DEFAULT_REDACT_BODY_FIELDS, ...opts.redactBodyFields ?? []];
962
+ let out = input;
963
+ for (const key of fields) {
964
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
965
+ out = out.replace(new RegExp(`("${escaped}"\\s*:\\s*)"[^"]*"`, "gi"), `$1"${REDACTED}"`);
966
+ }
967
+ return out;
702
968
  }
703
969
 
704
970
  // src/http-instrumentation.ts
@@ -708,12 +974,16 @@ var AXIOS_FLAG = /* @__PURE__ */ Symbol.for("allstak.http.axios.instrumented");
708
974
  var DEFAULT_MAX_BODY = 4096;
709
975
  var _currentModule = null;
710
976
  var _currentOpts = null;
977
+ var _currentRuntime = null;
711
978
  function currentModule() {
712
979
  return _currentModule;
713
980
  }
714
981
  function currentOpts() {
715
982
  return _currentOpts;
716
983
  }
984
+ function currentRuntime() {
985
+ return _currentRuntime;
986
+ }
717
987
  function safeCapture(ev) {
718
988
  try {
719
989
  currentModule()?.capture(ev);
@@ -730,6 +1000,8 @@ function bind(opts, ownIngestHost) {
730
1000
  ignoredUrls: opts.ignoredUrls ?? [],
731
1001
  allowedUrls: opts.allowedUrls ?? [],
732
1002
  maxBodyBytes: opts.maxBodyBytes ?? DEFAULT_MAX_BODY,
1003
+ allowedContentTypes: opts.allowedContentTypes ?? ["application/json", "text/", "application/problem+json"],
1004
+ redactBodyFields: opts.redactBodyFields ?? [],
733
1005
  ownIngestPrefix: ownIngestHost.replace(/\/$/, "")
734
1006
  };
735
1007
  }
@@ -776,6 +1048,77 @@ function headersToObject(h) {
776
1048
  }
777
1049
  return {};
778
1050
  }
1051
+ function normalizeTraceId(traceId) {
1052
+ const hex = traceId.replace(/[^a-fA-F0-9]/g, "").toLowerCase();
1053
+ return (hex + "00000000000000000000000000000000").slice(0, 32);
1054
+ }
1055
+ function normalizeSpanId(spanId) {
1056
+ const hex = spanId.replace(/[^a-fA-F0-9]/g, "").toLowerCase();
1057
+ return (hex + "0000000000000000").slice(0, 16);
1058
+ }
1059
+ function createRequestContext(method, url) {
1060
+ const runtime = currentRuntime();
1061
+ if (!runtime) return null;
1062
+ const requestId = generateHttpId();
1063
+ const parentSpanId = runtime.tracing.getCurrentSpanId() ?? "";
1064
+ const span = runtime.tracing.startSpan("mobile.http", {
1065
+ description: `${method.toUpperCase()} ${url}`,
1066
+ tags: { requestId, method: method.toUpperCase(), platform: runtime.platform ?? "react-native" }
1067
+ });
1068
+ const traceId = runtime.tracing.getTraceId();
1069
+ const spanId = span.spanId;
1070
+ return {
1071
+ traceId,
1072
+ requestId,
1073
+ spanId,
1074
+ parentSpanId,
1075
+ traceparent: `00-${normalizeTraceId(traceId)}-${normalizeSpanId(spanId)}-01`,
1076
+ span
1077
+ };
1078
+ }
1079
+ function propagationHeaders(ctx) {
1080
+ const headers = {
1081
+ traceparent: ctx.traceparent,
1082
+ "x-allstak-trace-id": ctx.traceId,
1083
+ "x-allstak-request-id": ctx.requestId
1084
+ };
1085
+ if (ctx.parentSpanId) headers["x-allstak-parent-span-id"] = ctx.parentSpanId;
1086
+ return headers;
1087
+ }
1088
+ function mergeHeaders(headers, propagation) {
1089
+ const entries = Object.entries(propagation);
1090
+ if (typeof Headers !== "undefined" && headers instanceof Headers) {
1091
+ const next2 = new Headers(headers);
1092
+ for (const [k, v] of entries) if (!next2.has(k)) next2.set(k, v);
1093
+ return next2;
1094
+ }
1095
+ if (Array.isArray(headers)) {
1096
+ const existing = new Set(headers.map(([k]) => String(k).toLowerCase()));
1097
+ const next2 = [...headers];
1098
+ for (const [k, v] of entries) if (!existing.has(k.toLowerCase())) next2.push([k, v]);
1099
+ return next2;
1100
+ }
1101
+ const next = { ...headers ?? {} };
1102
+ const lower = new Set(Object.keys(next).map((k) => k.toLowerCase()));
1103
+ for (const [k, v] of entries) if (!lower.has(k.toLowerCase())) next[k] = v;
1104
+ return next;
1105
+ }
1106
+ function injectFetchHeaders(input, init, ctx) {
1107
+ const headers = mergeHeaders(init?.headers ?? (input && typeof input === "object" ? input.headers : void 0), propagationHeaders(ctx));
1108
+ return { input, init: { ...init ?? {}, headers } };
1109
+ }
1110
+ function recordTimeline(kind, label, ctx, data) {
1111
+ try {
1112
+ currentRuntime()?.replay?.recordTimelineMarker?.(kind, label, data, {
1113
+ traceId: ctx.traceId,
1114
+ requestId: ctx.requestId,
1115
+ spanId: ctx.spanId,
1116
+ release: currentRuntime()?.release,
1117
+ dist: currentRuntime()?.dist
1118
+ });
1119
+ } catch {
1120
+ }
1121
+ }
779
1122
  function patchFetch() {
780
1123
  const g = globalThis;
781
1124
  if (typeof g.fetch !== "function") return;
@@ -793,23 +1136,37 @@ function patchFetch() {
793
1136
  return original.call(this, input, init);
794
1137
  }
795
1138
  const start = Date.now();
796
- const reqHeaders = sanitizeHeaders(headersToObject(init?.headers ?? (input && input.headers)), opts);
797
- const reqBody = captureBody(init?.body, opts.captureRequestBody, opts.maxBodyBytes);
1139
+ const ctx = createRequestContext(method, sanitizedUrl);
1140
+ const requestInit = ctx ? injectFetchHeaders(input, init, ctx).init : init;
1141
+ if (ctx) recordTimeline("request", "request_started", ctx, { method, url: sanitizedUrl });
1142
+ const reqHeaders = sanitizeHeaders(headersToObject(requestInit?.headers ?? (input && input.headers)), opts);
1143
+ const reqBody = captureBodyResult(requestInit?.body, opts.captureRequestBody, opts.maxBodyBytes, opts, reqHeaders?.["content-type"]);
798
1144
  const reqSize = safeByteLength(typeof init?.body === "string" ? init.body : void 0);
799
1145
  let response;
800
1146
  try {
801
- response = await original.call(this, input, init);
1147
+ response = await original.call(this, input, requestInit);
802
1148
  } catch (err) {
1149
+ ctx?.span.finish("error");
1150
+ if (ctx) recordTimeline("exception", "request_failed", ctx, { method, url: sanitizedUrl, error: String(err?.message ?? err) });
803
1151
  safeCapture({
804
1152
  type: "http_request",
1153
+ traceId: ctx?.traceId ?? generateHttpId(),
1154
+ requestId: ctx?.requestId ?? generateHttpId(),
1155
+ spanId: ctx?.spanId,
1156
+ parentSpanId: ctx?.parentSpanId,
805
1157
  method,
806
1158
  url: sanitizedUrl,
807
1159
  statusCode: 0,
808
1160
  durationMs: Date.now() - start,
809
- requestBody: reqBody,
1161
+ requestBody: reqBody.body,
810
1162
  requestHeaders: reqHeaders,
811
1163
  requestSize: reqSize,
812
- error: String(err?.message ?? err)
1164
+ requestBodyStatus: reqBody.status,
1165
+ requestBodyRedactedFields: reqBody.redactedFields,
1166
+ requestBodyTruncated: reqBody.truncated,
1167
+ capturePolicy: reqBody.capturePolicy,
1168
+ error: String(err?.message ?? err),
1169
+ linkConfidence: "exact"
813
1170
  });
814
1171
  throw err;
815
1172
  }
@@ -825,25 +1182,41 @@ function patchFetch() {
825
1182
  try {
826
1183
  const cloned = response.clone();
827
1184
  const text = await cloned.text();
828
- respBody = captureBody(text, true, opts.maxBodyBytes);
1185
+ respBody = captureBodyResult(text, true, opts.maxBodyBytes, opts, respHeaders?.["content-type"]).body;
829
1186
  if (respSize == null) respSize = safeByteLength(text);
830
1187
  } catch {
831
1188
  }
832
1189
  }
833
1190
  } catch {
834
1191
  }
1192
+ const respBodyResult = captureBodyResult(respBody, opts.captureResponseBody && respBody !== void 0, opts.maxBodyBytes, opts, respHeaders?.["content-type"]);
1193
+ const isError = response.status >= 400;
1194
+ ctx?.span.setTag("requestId", ctx.requestId).setTag("http.status_code", String(response.status)).finish(isError ? "error" : "ok");
1195
+ if (ctx) recordTimeline(isError ? "exception" : "response", isError ? "request_failed" : "response_received", ctx, { statusCode: response.status, durationMs });
835
1196
  safeCapture({
836
1197
  type: "http_request",
1198
+ traceId: ctx?.traceId ?? generateHttpId(),
1199
+ requestId: ctx?.requestId ?? generateHttpId(),
1200
+ spanId: ctx?.spanId,
1201
+ parentSpanId: ctx?.parentSpanId,
837
1202
  method,
838
1203
  url: sanitizedUrl,
839
1204
  statusCode: response.status,
840
1205
  durationMs,
841
- requestBody: reqBody,
1206
+ requestBody: reqBody.body,
842
1207
  requestHeaders: reqHeaders,
843
1208
  requestSize: reqSize,
844
- responseBody: respBody,
1209
+ responseBody: respBodyResult.body,
845
1210
  responseHeaders: respHeaders,
846
- responseSize: respSize
1211
+ responseSize: respSize,
1212
+ requestBodyStatus: reqBody.status,
1213
+ responseBodyStatus: respBodyResult.status,
1214
+ requestBodyRedactedFields: reqBody.redactedFields,
1215
+ responseBodyRedactedFields: respBodyResult.redactedFields,
1216
+ requestBodyTruncated: reqBody.truncated,
1217
+ responseBodyTruncated: respBodyResult.truncated,
1218
+ capturePolicy: `${reqBody.capturePolicy};${respBodyResult.capturePolicy}`,
1219
+ linkConfidence: "exact"
847
1220
  });
848
1221
  return response;
849
1222
  };
@@ -881,7 +1254,17 @@ function patchXhr() {
881
1254
  return origSend.call(this, body);
882
1255
  }
883
1256
  const reqHeaders = sanitizeHeaders(this.__allstak_headers__, opts);
884
- const reqBody = captureBody(body, opts.captureRequestBody, opts.maxBodyBytes);
1257
+ const ctx = createRequestContext(method, sanitizedUrl);
1258
+ if (ctx) {
1259
+ for (const [k, v] of Object.entries(propagationHeaders(ctx))) {
1260
+ try {
1261
+ origSetRequestHeader.call(this, k, v);
1262
+ } catch {
1263
+ }
1264
+ }
1265
+ recordTimeline("request", "request_started", ctx, { method, url: sanitizedUrl });
1266
+ }
1267
+ const reqBody = captureBodyResult(body, opts.captureRequestBody, opts.maxBodyBytes, opts, reqHeaders?.["content-type"]);
885
1268
  const reqSize = safeByteLength(typeof body === "string" ? body : void 0);
886
1269
  const finish = (statusCode, error) => {
887
1270
  const durationMs = Date.now() - start;
@@ -899,28 +1282,43 @@ function patchXhr() {
899
1282
  }
900
1283
  respHeaders = sanitizeHeaders(dict, liveOpts);
901
1284
  }
1285
+ let respBodyResult = captureBodyResult(void 0, false, liveOpts.maxBodyBytes, liveOpts);
902
1286
  if (liveOpts.captureResponseBody) {
903
1287
  const text = this.responseText;
904
1288
  if (typeof text === "string") {
905
- respBody = captureBody(text, true, liveOpts.maxBodyBytes);
1289
+ respBodyResult = captureBodyResult(text, true, liveOpts.maxBodyBytes, liveOpts, respHeaders?.["content-type"]);
1290
+ respBody = respBodyResult.body;
906
1291
  respSize = safeByteLength(text);
907
1292
  }
908
1293
  }
909
1294
  } catch {
910
1295
  }
1296
+ const failed = !!error || statusCode >= 400;
1297
+ ctx?.span.setTag("requestId", ctx.requestId).setTag("http.status_code", String(statusCode)).finish(failed ? "error" : "ok");
1298
+ if (ctx) recordTimeline(failed ? "exception" : "response", failed ? "request_failed" : "response_received", ctx, { statusCode, durationMs, error });
911
1299
  safeCapture({
912
1300
  type: "http_request",
1301
+ traceId: ctx?.traceId ?? generateHttpId(),
1302
+ requestId: ctx?.requestId ?? generateHttpId(),
1303
+ spanId: ctx?.spanId,
1304
+ parentSpanId: ctx?.parentSpanId,
913
1305
  method,
914
1306
  url: sanitizedUrl,
915
1307
  statusCode,
916
1308
  durationMs,
917
- requestBody: reqBody,
1309
+ requestBody: reqBody.body,
918
1310
  requestHeaders: reqHeaders,
919
1311
  requestSize: reqSize,
920
1312
  responseBody: respBody,
921
1313
  responseHeaders: respHeaders,
922
1314
  responseSize: respSize,
923
- error
1315
+ requestBodyStatus: reqBody.status,
1316
+ responseBodyStatus: respBody ? "captured" : opts.captureResponseBody ? "empty" : "disabled",
1317
+ requestBodyRedactedFields: reqBody.redactedFields,
1318
+ requestBodyTruncated: reqBody.truncated,
1319
+ capturePolicy: reqBody.capturePolicy,
1320
+ error,
1321
+ linkConfidence: "exact"
924
1322
  });
925
1323
  };
926
1324
  this.addEventListener?.("load", () => finish(this.status || 0));
@@ -939,10 +1337,17 @@ function instrumentAxiosInstance(axiosInstance, module2, opts) {
939
1337
  axiosInstance.interceptors.request.use((config) => {
940
1338
  try {
941
1339
  const rawUrl = (config.baseURL ? config.baseURL.replace(/\/$/, "") : "") + (config.url || "");
1340
+ const method = String(config.method || "GET").toUpperCase();
1341
+ const ctx = createRequestContext(method, rawUrl);
1342
+ if (ctx) {
1343
+ config.headers = mergeHeaders(config.headers, propagationHeaders(ctx));
1344
+ recordTimeline("request", "request_started", ctx, { method, url: rawUrl });
1345
+ }
942
1346
  reqStarts.set(config, {
943
1347
  start: Date.now(),
944
- method: String(config.method || "GET").toUpperCase(),
945
- rawUrl
1348
+ method,
1349
+ rawUrl,
1350
+ ctx
946
1351
  });
947
1352
  } catch {
948
1353
  }
@@ -956,21 +1361,36 @@ function instrumentAxiosInstance(axiosInstance, module2, opts) {
956
1361
  if (!shouldCaptureUrl(meta.rawUrl, opts)) return;
957
1362
  const sanitizedUrl = redactUrl(meta.rawUrl, opts);
958
1363
  const reqHeaders = sanitizeHeaders(headersToObject(cfg.headers), opts);
959
- const reqBody = captureBody(cfg.data, opts.captureRequestBody, opts.maxBodyBytes);
1364
+ const reqBody = captureBodyResult(cfg.data, opts.captureRequestBody, opts.maxBodyBytes, opts, reqHeaders?.["content-type"]);
960
1365
  const respHeaders = sanitizeHeaders(headersToObject(response?.headers), opts);
961
- const respBody = captureBody(response?.data, opts.captureResponseBody, opts.maxBodyBytes);
1366
+ const respBody = captureBodyResult(response?.data, opts.captureResponseBody, opts.maxBodyBytes, opts, respHeaders?.["content-type"]);
1367
+ const failed = !!error || statusCode >= 400;
1368
+ meta.ctx?.span.setTag("requestId", meta.ctx.requestId).setTag("http.status_code", String(statusCode)).finish(failed ? "error" : "ok");
1369
+ if (meta.ctx) recordTimeline(failed ? "exception" : "response", failed ? "request_failed" : "response_received", meta.ctx, { statusCode, error });
962
1370
  try {
963
1371
  module2.capture({
964
1372
  type: "http_request",
1373
+ traceId: meta.ctx?.traceId ?? generateHttpId(),
1374
+ requestId: meta.ctx?.requestId ?? generateHttpId(),
1375
+ spanId: meta.ctx?.spanId,
1376
+ parentSpanId: meta.ctx?.parentSpanId,
965
1377
  method: meta.method,
966
1378
  url: sanitizedUrl,
967
1379
  statusCode,
968
1380
  durationMs: Date.now() - meta.start,
969
- requestBody: reqBody,
1381
+ requestBody: reqBody.body,
970
1382
  requestHeaders: reqHeaders,
971
- responseBody: respBody,
1383
+ responseBody: respBody.body,
972
1384
  responseHeaders: respHeaders,
973
- error
1385
+ requestBodyStatus: reqBody.status,
1386
+ responseBodyStatus: respBody.status,
1387
+ requestBodyRedactedFields: reqBody.redactedFields,
1388
+ responseBodyRedactedFields: respBody.redactedFields,
1389
+ requestBodyTruncated: reqBody.truncated,
1390
+ responseBodyTruncated: respBody.truncated,
1391
+ capturePolicy: `${reqBody.capturePolicy};${respBody.capturePolicy}`,
1392
+ error,
1393
+ linkConfidence: "exact"
974
1394
  });
975
1395
  } catch {
976
1396
  }
@@ -996,10 +1416,11 @@ function tryAutoInstrumentAxios(module2, opts) {
996
1416
  } catch {
997
1417
  }
998
1418
  }
999
- function installHttpInstrumentation(module2, options, ownIngestHost) {
1419
+ function installHttpInstrumentation(module2, options, ownIngestHost, runtime) {
1000
1420
  const bound = bind(options, ownIngestHost);
1001
1421
  _currentModule = module2;
1002
1422
  _currentOpts = bound;
1423
+ _currentRuntime = runtime;
1003
1424
  try {
1004
1425
  patchFetch();
1005
1426
  } catch {
@@ -1016,6 +1437,11 @@ function installHttpInstrumentation(module2, options, ownIngestHost) {
1016
1437
  instrumentAxios: (axios) => instrumentAxiosInstance(axios, module2, bound)
1017
1438
  };
1018
1439
  }
1440
+ function unbindHttpInstrumentation() {
1441
+ _currentModule = null;
1442
+ _currentOpts = null;
1443
+ _currentRuntime = null;
1444
+ }
1019
1445
 
1020
1446
  // src/client.ts
1021
1447
  var INGEST_HOST = "https://api.allstak.sa";
@@ -1036,6 +1462,17 @@ function generateId() {
1036
1462
  const seg = (len) => Array.from({ length: len }, () => hex(16)).join("");
1037
1463
  return `${seg(8)}-${seg(4)}-4${seg(3)}-${(8 + Math.floor(Math.random() * 4)).toString(16)}${seg(3)}-${seg(12)}`;
1038
1464
  }
1465
+ function stringContextValue(context, key) {
1466
+ const value = context[key];
1467
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
1468
+ }
1469
+ function firstRecentRequestId(recentFailed) {
1470
+ for (let i = recentFailed.length - 1; i >= 0; i--) {
1471
+ const requestId = recentFailed[i]?.requestId;
1472
+ if (requestId && requestId.trim().length > 0) return requestId;
1473
+ }
1474
+ return void 0;
1475
+ }
1039
1476
  var AllStakClient = class {
1040
1477
  constructor(config) {
1041
1478
  this.breadcrumbs = [];
@@ -1043,9 +1480,6 @@ var AllStakClient = class {
1043
1480
  this.replay = null;
1044
1481
  this.httpRequests = null;
1045
1482
  this._instrumentAxios = null;
1046
- if (!config.apiKey) {
1047
- throw new Error("AllStak: config.apiKey is required");
1048
- }
1049
1483
  this.config = { ...config };
1050
1484
  if (!this.config.environment) this.config.environment = "production";
1051
1485
  if (!this.config.sdkName) this.config.sdkName = SDK_NAME;
@@ -1054,7 +1488,7 @@ var AllStakClient = class {
1054
1488
  this.sessionId = generateId();
1055
1489
  this.maxBreadcrumbs = config.maxBreadcrumbs ?? DEFAULT_MAX_BREADCRUMBS;
1056
1490
  const baseUrl = (config.host ?? INGEST_HOST).replace(/\/$/, "");
1057
- this.transport = new HttpTransport(baseUrl, config.apiKey);
1491
+ this.transport = new HttpTransport(baseUrl, config.apiKey ?? "", Boolean(config.apiKey));
1058
1492
  this.tracing = new TracingModule(this.transport, {
1059
1493
  service: config.service ?? config.release ?? "",
1060
1494
  environment: this.config.environment ?? "production",
@@ -1081,7 +1515,15 @@ var AllStakClient = class {
1081
1515
  const { instrumentAxios } = installHttpInstrumentation(
1082
1516
  this.httpRequests,
1083
1517
  config.httpTracking ?? {},
1084
- baseUrl
1518
+ baseUrl,
1519
+ {
1520
+ tracing: this.tracing,
1521
+ replay: this.replay,
1522
+ release: this.config.release,
1523
+ dist: this.config.dist,
1524
+ platform: this.config.platform,
1525
+ environment: this.config.environment
1526
+ }
1085
1527
  );
1086
1528
  this._instrumentAxios = instrumentAxios;
1087
1529
  } catch {
@@ -1105,33 +1547,67 @@ var AllStakClient = class {
1105
1547
  if (!this.passesSampleRate()) return;
1106
1548
  const frames = parseStack(error.stack).map((f) => ({
1107
1549
  ...f,
1108
- platform: this.config.platform
1550
+ platform: this.config.platform,
1551
+ debugId: resolveDebugId(f.filename)
1109
1552
  }));
1553
+ const debugIdSet = /* @__PURE__ */ new Set();
1554
+ for (const f of frames) if (f.debugId) debugIdSet.add(f.debugId);
1555
+ const debugMeta = debugIdSet.size > 0 ? { images: Array.from(debugIdSet).map((id2) => ({ type: "sourcemap", debugId: id2 })) } : void 0;
1110
1556
  const stackTrace = frames.length > 0 ? frames.map(frameToString) : void 0;
1111
1557
  const currentBreadcrumbs = this.breadcrumbs.length > 0 ? [...this.breadcrumbs] : void 0;
1112
1558
  this.breadcrumbs = [];
1113
1559
  const exceptionClass = (error.name && error.name !== "Error" ? error.name : void 0) || error.constructor?.name || "Error";
1114
1560
  const eff = this.effective();
1115
1561
  const traceContext = {};
1116
- const traceId = this.tracing.getTraceId();
1562
+ const recentFailed = this.httpRequests?.getRecentFailed() ?? [];
1563
+ const linkedRequest = recentFailed.length > 0 ? recentFailed[recentFailed.length - 1] : void 0;
1564
+ if (linkedRequest?.traceId) this.tracing.setTraceId(linkedRequest.traceId);
1565
+ const exceptionSpan = linkedRequest ? this.tracing.startSpan("mobile.exception", {
1566
+ description: error.message,
1567
+ tags: {
1568
+ requestId: linkedRequest.requestId,
1569
+ exceptionClass
1570
+ }
1571
+ }) : null;
1572
+ exceptionSpan?.finish("error");
1573
+ const traceId = linkedRequest?.traceId ?? this.tracing.getTraceId();
1117
1574
  if (traceId) traceContext.traceId = traceId;
1118
- const spanId = this.tracing.getCurrentSpanId();
1575
+ const spanId = exceptionSpan?.spanId || this.tracing.getCurrentSpanId();
1119
1576
  if (spanId) traceContext.spanId = spanId;
1120
- const recentFailed = this.httpRequests?.getRecentFailed() ?? [];
1121
1577
  if (recentFailed.length > 0) {
1122
1578
  traceContext["http.recentFailed"] = recentFailed.map((r) => ({
1123
1579
  method: r.method,
1124
1580
  url: r.url,
1125
1581
  statusCode: r.statusCode,
1126
1582
  durationMs: r.durationMs,
1127
- error: r.error
1583
+ error: r.error,
1584
+ requestId: r.requestId,
1585
+ traceId: r.traceId,
1586
+ confidence: r.requestId === linkedRequest?.requestId ? "inferred" : "weak"
1128
1587
  }));
1588
+ traceContext["http.linkConfidence"] = "inferred";
1589
+ }
1590
+ try {
1591
+ if (!linkedRequest) throw new Error("no linked request");
1592
+ this.replay?.recordTimelineMarker?.("exception", "exception_captured", {
1593
+ exceptionClass,
1594
+ message: error.message,
1595
+ requestLinkConfidence: linkedRequest ? "inferred" : "none"
1596
+ }, {
1597
+ traceId,
1598
+ requestId: linkedRequest?.requestId,
1599
+ spanId: spanId ?? void 0,
1600
+ release: this.config.release,
1601
+ dist: this.config.dist
1602
+ });
1603
+ } catch {
1129
1604
  }
1130
1605
  const payload = {
1131
1606
  exceptionClass,
1132
1607
  message: error.message,
1133
1608
  stackTrace,
1134
1609
  frames: frames.length > 0 ? frames : void 0,
1610
+ debugMeta,
1135
1611
  platform: this.config.platform,
1136
1612
  sdkName: this.config.sdkName,
1137
1613
  sdkVersion: this.config.sdkVersion,
@@ -1140,12 +1616,29 @@ var AllStakClient = class {
1140
1616
  environment: this.config.environment,
1141
1617
  release: this.config.release,
1142
1618
  sessionId: this.sessionId,
1619
+ traceId: stringContextValue(traceContext, "traceId"),
1620
+ spanId: stringContextValue(traceContext, "spanId"),
1621
+ requestId: linkedRequest?.requestId ?? firstRecentRequestId(recentFailed),
1622
+ service: this.config.service,
1143
1623
  user: eff.user,
1144
1624
  metadata: { ...this.buildMetadata(context), ...traceContext },
1145
1625
  breadcrumbs: currentBreadcrumbs,
1146
1626
  fingerprint: eff.fingerprint
1147
1627
  };
1148
- this.sendThroughBeforeSend(payload);
1628
+ if (this.shouldCaptureScreenshot()) {
1629
+ void this.withScreenshotMetadata(error, payload).then((enriched) => this.sendThroughBeforeSend(enriched)).catch(() => this.sendThroughBeforeSend({
1630
+ ...payload,
1631
+ metadata: { ...payload.metadata ?? {}, "screenshot.status": "failed" }
1632
+ }));
1633
+ return;
1634
+ }
1635
+ this.sendThroughBeforeSend({
1636
+ ...payload,
1637
+ metadata: {
1638
+ ...payload.metadata ?? {},
1639
+ "screenshot.status": this.config.screenshot?.enabled ? "unsupported" : "disabled"
1640
+ }
1641
+ });
1149
1642
  }
1150
1643
  /** Start a new span. Auto-parented to any currently-active span. */
1151
1644
  startSpan(operation, options) {
@@ -1186,6 +1679,9 @@ var AllStakClient = class {
1186
1679
  environment: this.config.environment,
1187
1680
  release: this.config.release,
1188
1681
  sessionId: this.sessionId,
1682
+ traceId: this.tracing.getTraceId(),
1683
+ spanId: this.tracing.getCurrentSpanId() ?? void 0,
1684
+ service: this.config.service,
1189
1685
  user: eff.user,
1190
1686
  metadata: this.buildMetadata(),
1191
1687
  fingerprint: eff.fingerprint
@@ -1269,6 +1765,9 @@ var AllStakClient = class {
1269
1765
  getConfig() {
1270
1766
  return this.config;
1271
1767
  }
1768
+ getTransportStats() {
1769
+ return this.transport.getStats();
1770
+ }
1272
1771
  destroy() {
1273
1772
  this.tracing.destroy();
1274
1773
  if (this.replay) {
@@ -1279,6 +1778,7 @@ var AllStakClient = class {
1279
1778
  this.httpRequests.destroy();
1280
1779
  this.httpRequests = null;
1281
1780
  }
1781
+ unbindHttpInstrumentation();
1282
1782
  this._instrumentAxios = null;
1283
1783
  this.breadcrumbs = [];
1284
1784
  }
@@ -1297,6 +1797,58 @@ var AllStakClient = class {
1297
1797
  metadata: this.buildMetadata()
1298
1798
  });
1299
1799
  }
1800
+ shouldCaptureScreenshot() {
1801
+ const screenshot = this.config.screenshot;
1802
+ if (!screenshot?.enabled || screenshot.captureOnError === false || !screenshot.provider) return false;
1803
+ const sampleRate = screenshot.sampleRate ?? 1;
1804
+ return !(sampleRate <= 0 || sampleRate < 1 && Math.random() >= sampleRate);
1805
+ }
1806
+ async withScreenshotMetadata(error, payload) {
1807
+ const screenshot = this.config.screenshot;
1808
+ if (!screenshot?.provider) {
1809
+ return { ...payload, metadata: { ...payload.metadata ?? {}, "screenshot.status": "unsupported" } };
1810
+ }
1811
+ const timeoutMs = Math.max(100, Math.min(screenshot.timeoutMs ?? 1500, 5e3));
1812
+ const maxBytes = Math.max(1024, screenshot.maxBytes ?? 2e5);
1813
+ try {
1814
+ const artifact = await Promise.race([
1815
+ Promise.resolve(screenshot.provider({
1816
+ type: "error",
1817
+ error,
1818
+ traceId: payload.traceId,
1819
+ requestId: payload.requestId
1820
+ })),
1821
+ new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
1822
+ ]);
1823
+ if (!artifact) {
1824
+ return { ...payload, metadata: { ...payload.metadata ?? {}, "screenshot.status": "timeout_or_empty" } };
1825
+ }
1826
+ const size = artifact.sizeBytes ?? byteSize(artifact.data);
1827
+ if (size > maxBytes) {
1828
+ this.transport.noteDropped();
1829
+ return {
1830
+ ...payload,
1831
+ metadata: { ...payload.metadata ?? {}, "screenshot.status": "dropped_too_large", "screenshot.sizeBytes": size }
1832
+ };
1833
+ }
1834
+ return {
1835
+ ...payload,
1836
+ metadata: {
1837
+ ...payload.metadata ?? {},
1838
+ "screenshot.status": "captured",
1839
+ "screenshot.contentType": artifact.contentType,
1840
+ "screenshot.width": artifact.width,
1841
+ "screenshot.height": artifact.height,
1842
+ "screenshot.sizeBytes": size,
1843
+ "screenshot.redacted": artifact.redacted ?? false,
1844
+ "screenshot.redactionStrategy": artifact.redactionStrategy,
1845
+ ...artifact.data ? { "screenshot.data": artifact.data } : {}
1846
+ }
1847
+ };
1848
+ } catch {
1849
+ return { ...payload, metadata: { ...payload.metadata ?? {}, "screenshot.status": "failed" } };
1850
+ }
1851
+ }
1300
1852
  passesSampleRate() {
1301
1853
  const r = this.config.sampleRate;
1302
1854
  if (typeof r !== "number" || r >= 1) return true;
@@ -1395,10 +1947,22 @@ var AllStakClient = class {
1395
1947
  }
1396
1948
  };
1397
1949
  var instance = null;
1398
- function ensureInit() {
1399
- if (!instance) throw new Error("AllStak.init() must be called before using the SDK");
1950
+ function maybeInit() {
1400
1951
  return instance;
1401
1952
  }
1953
+ function noopSpan(operation = "") {
1954
+ return new Span("", "", "", operation, "", "", "", {}, () => void 0);
1955
+ }
1956
+ function emptyStats() {
1957
+ return {
1958
+ queued: 0,
1959
+ sent: 0,
1960
+ failed: 0,
1961
+ dropped: 0,
1962
+ consecutiveFailures: 0,
1963
+ circuitOpenUntil: 0
1964
+ };
1965
+ }
1402
1966
  function __safeAddBreadcrumbForInstrumentation(type, message, level, data) {
1403
1967
  try {
1404
1968
  instance?.addBreadcrumb(type, message, level, data);
@@ -1407,51 +1971,99 @@ function __safeAddBreadcrumbForInstrumentation(type, message, level, data) {
1407
1971
  }
1408
1972
  var AllStak = {
1409
1973
  init(config) {
1410
- if (instance) instance.destroy();
1411
- instance = new AllStakClient(config);
1412
- return instance;
1974
+ try {
1975
+ if (instance) instance.destroy();
1976
+ instance = new AllStakClient(config);
1977
+ return instance;
1978
+ } catch {
1979
+ instance = new AllStakClient({ ...config, apiKey: "" });
1980
+ return instance;
1981
+ }
1413
1982
  },
1414
1983
  captureException(error, context) {
1415
- ensureInit().captureException(error, context);
1984
+ try {
1985
+ maybeInit()?.captureException(error, context);
1986
+ } catch {
1987
+ }
1416
1988
  },
1417
1989
  captureMessage(message, level = "info", options) {
1418
- ensureInit().captureMessage(message, level, options);
1990
+ try {
1991
+ maybeInit()?.captureMessage(message, level, options);
1992
+ } catch {
1993
+ }
1419
1994
  },
1420
1995
  addBreadcrumb(type, message, level, data) {
1421
- ensureInit().addBreadcrumb(type, message, level, data);
1996
+ try {
1997
+ maybeInit()?.addBreadcrumb(type, message, level, data);
1998
+ } catch {
1999
+ }
1422
2000
  },
1423
2001
  clearBreadcrumbs() {
1424
- ensureInit().clearBreadcrumbs();
2002
+ try {
2003
+ maybeInit()?.clearBreadcrumbs();
2004
+ } catch {
2005
+ }
1425
2006
  },
1426
2007
  setUser(user) {
1427
- ensureInit().setUser(user);
2008
+ try {
2009
+ maybeInit()?.setUser(user);
2010
+ } catch {
2011
+ }
1428
2012
  },
1429
2013
  setTag(key, value) {
1430
- ensureInit().setTag(key, value);
2014
+ try {
2015
+ maybeInit()?.setTag(key, value);
2016
+ } catch {
2017
+ }
1431
2018
  },
1432
2019
  setTags(tags) {
1433
- ensureInit().setTags(tags);
2020
+ try {
2021
+ maybeInit()?.setTags(tags);
2022
+ } catch {
2023
+ }
1434
2024
  },
1435
2025
  setExtra(key, value) {
1436
- ensureInit().setExtra(key, value);
2026
+ try {
2027
+ maybeInit()?.setExtra(key, value);
2028
+ } catch {
2029
+ }
1437
2030
  },
1438
2031
  setExtras(extras) {
1439
- ensureInit().setExtras(extras);
2032
+ try {
2033
+ maybeInit()?.setExtras(extras);
2034
+ } catch {
2035
+ }
1440
2036
  },
1441
2037
  setContext(name, ctx) {
1442
- ensureInit().setContext(name, ctx);
2038
+ try {
2039
+ maybeInit()?.setContext(name, ctx);
2040
+ } catch {
2041
+ }
1443
2042
  },
1444
2043
  setLevel(level) {
1445
- ensureInit().setLevel(level);
2044
+ try {
2045
+ maybeInit()?.setLevel(level);
2046
+ } catch {
2047
+ }
1446
2048
  },
1447
2049
  setFingerprint(fingerprint) {
1448
- ensureInit().setFingerprint(fingerprint);
2050
+ try {
2051
+ maybeInit()?.setFingerprint(fingerprint);
2052
+ } catch {
2053
+ }
1449
2054
  },
1450
2055
  flush(timeoutMs) {
1451
- return ensureInit().flush(timeoutMs);
2056
+ try {
2057
+ return maybeInit()?.flush(timeoutMs) ?? Promise.resolve(true);
2058
+ } catch {
2059
+ return Promise.resolve(false);
2060
+ }
1452
2061
  },
1453
2062
  setIdentity(identity) {
1454
- ensureInit().setIdentity(identity);
2063
+ try {
2064
+ maybeInit()?.setIdentity(identity);
2065
+ } catch {
2066
+ }
1455
2067
  },
1456
2068
  /**
1457
2069
  * Run `callback` with a fresh scoped context. Any user/tag/extra/context/
@@ -1460,37 +2072,79 @@ var AllStak = {
1460
2072
  * for async callbacks and thrown errors.
1461
2073
  */
1462
2074
  withScope(callback) {
1463
- return ensureInit().withScope(callback);
2075
+ try {
2076
+ const client = maybeInit();
2077
+ return client ? client.withScope(callback) : callback(new Scope());
2078
+ } catch {
2079
+ return callback(new Scope());
2080
+ }
1464
2081
  },
1465
2082
  startSpan(operation, options) {
1466
- return ensureInit().startSpan(operation, options);
2083
+ try {
2084
+ return maybeInit()?.startSpan(operation, options) ?? noopSpan(operation);
2085
+ } catch {
2086
+ return noopSpan(operation);
2087
+ }
1467
2088
  },
1468
2089
  getTraceId() {
1469
- return ensureInit().getTraceId();
2090
+ try {
2091
+ return maybeInit()?.getTraceId() ?? "";
2092
+ } catch {
2093
+ return "";
2094
+ }
1470
2095
  },
1471
2096
  setTraceId(traceId) {
1472
- ensureInit().setTraceId(traceId);
2097
+ try {
2098
+ maybeInit()?.setTraceId(traceId);
2099
+ } catch {
2100
+ }
1473
2101
  },
1474
2102
  getCurrentSpanId() {
1475
- return ensureInit().getCurrentSpanId();
2103
+ try {
2104
+ return maybeInit()?.getCurrentSpanId() ?? null;
2105
+ } catch {
2106
+ return null;
2107
+ }
1476
2108
  },
1477
2109
  resetTrace() {
1478
- ensureInit().resetTrace();
2110
+ try {
2111
+ maybeInit()?.resetTrace();
2112
+ } catch {
2113
+ }
1479
2114
  },
1480
2115
  /** Access the privacy-first replay surrogate (or null if disabled / sampled out). */
1481
2116
  getReplay() {
1482
- return ensureInit().getReplay();
2117
+ try {
2118
+ return maybeInit()?.getReplay() ?? null;
2119
+ } catch {
2120
+ return null;
2121
+ }
1483
2122
  },
1484
2123
  /** Manually instrument an axios instance. No-op when HTTP tracking is off. */
1485
2124
  instrumentAxios(axios) {
1486
- return ensureInit().instrumentAxios(axios);
2125
+ try {
2126
+ return maybeInit()?.instrumentAxios(axios) ?? axios;
2127
+ } catch {
2128
+ return axios;
2129
+ }
1487
2130
  },
1488
2131
  getSessionId() {
1489
- return ensureInit().getSessionId();
2132
+ try {
2133
+ return maybeInit()?.getSessionId() ?? "";
2134
+ } catch {
2135
+ return "";
2136
+ }
1490
2137
  },
1491
2138
  getConfig() {
1492
2139
  return instance?.getConfig() ?? null;
1493
2140
  },
2141
+ getTransportStats() {
2142
+ try {
2143
+ return maybeInit()?.getTransportStats() ?? emptyStats();
2144
+ } catch {
2145
+ return emptyStats();
2146
+ }
2147
+ },
1494
2148
  destroy() {
1495
2149
  instance?.destroy();
1496
2150
  instance = null;
@@ -1500,6 +2154,14 @@ var AllStak = {
1500
2154
  return instance;
1501
2155
  }
1502
2156
  };
2157
+ function byteSize(value) {
2158
+ if (!value) return 0;
2159
+ try {
2160
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(value).length;
2161
+ } catch {
2162
+ }
2163
+ return value.length;
2164
+ }
1503
2165
 
1504
2166
  // src/provider.tsx
1505
2167
  var React = __toESM(require("react"));
@@ -1859,7 +2521,7 @@ function installReactNative(options = {}) {
1859
2521
  }
1860
2522
  AllStak.setIdentity({
1861
2523
  sdkName: "allstak-react-native",
1862
- sdkVersion: "0.3.0",
2524
+ sdkVersion: SDK_VERSION,
1863
2525
  platform: "react-native",
1864
2526
  dist
1865
2527
  });