@allstak/react 0.2.1 → 0.3.1

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.mjs CHANGED
@@ -143,12 +143,12 @@ function instrumentFetch(addBreadcrumb, ownBaseUrl) {
143
143
  else if (input && typeof input.url === "string") url = input.url;
144
144
  else url = String(input);
145
145
  const safePath = url.split("?")[0];
146
- const isOwnIngest = !!(ownBaseUrl && url.startsWith(ownBaseUrl));
146
+ const isOwnIngest2 = !!(ownBaseUrl && url.startsWith(ownBaseUrl));
147
147
  const start = Date.now();
148
148
  try {
149
149
  const response = await originalFetch.call(this, input, init);
150
150
  const durationMs = Date.now() - start;
151
- if (!isOwnIngest) {
151
+ if (!isOwnIngest2) {
152
152
  addBreadcrumb(
153
153
  "http",
154
154
  `${method} ${safePath} -> ${response.status}`,
@@ -159,7 +159,7 @@ function instrumentFetch(addBreadcrumb, ownBaseUrl) {
159
159
  return response;
160
160
  } catch (err) {
161
161
  const durationMs = Date.now() - start;
162
- if (!isOwnIngest) {
162
+ if (!isOwnIngest2) {
163
163
  addBreadcrumb("http", `${method} ${safePath} -> failed`, "error", {
164
164
  method,
165
165
  url: safePath,
@@ -695,10 +695,534 @@ function resolveDebugId(filename) {
695
695
  return id2;
696
696
  }
697
697
 
698
+ // src/http-requests.ts
699
+ var INGEST_PATH = "/ingest/v1/http-requests";
700
+ var FLUSH_INTERVAL_MS3 = 5e3;
701
+ var BATCH_SIZE_THRESHOLD2 = 20;
702
+ var RECENT_FAILED_BUFFER_SIZE = 10;
703
+ function genTraceId() {
704
+ const hex = (n) => Math.floor(Math.random() * n).toString(16).padStart(1, "0");
705
+ const seg = (len) => Array.from({ length: len }, () => hex(16)).join("");
706
+ return `${seg(8)}-${seg(4)}-4${seg(3)}-${(8 + Math.floor(Math.random() * 4)).toString(16)}${seg(3)}-${seg(12)}`;
707
+ }
708
+ function splitHostPath(url) {
709
+ try {
710
+ const u = new URL(url);
711
+ return { host: u.host, path: u.pathname || "/" };
712
+ } catch {
713
+ return { host: "", path: url.split("?")[0] };
714
+ }
715
+ }
716
+ var HttpRequestModule = class {
717
+ constructor(transport) {
718
+ this.transport = transport;
719
+ this.queue = [];
720
+ this.recentFailed = [];
721
+ this.flushTimer = null;
722
+ this.destroyed = false;
723
+ this.defaults = {};
724
+ }
725
+ setDefaults(defaults) {
726
+ this.defaults = { ...this.defaults, ...defaults };
727
+ }
728
+ capture(ev) {
729
+ if (this.destroyed) return;
730
+ const { host, path } = splitHostPath(ev.url);
731
+ const item = {
732
+ type: "http_request",
733
+ traceId: ev.traceId ?? genTraceId(),
734
+ direction: "outbound",
735
+ method: (ev.method || "GET").toUpperCase(),
736
+ host,
737
+ path,
738
+ url: ev.url,
739
+ statusCode: ev.statusCode ?? 0,
740
+ durationMs: Math.max(0, Math.floor(ev.durationMs)),
741
+ requestSize: ev.requestSize,
742
+ responseSize: ev.responseSize,
743
+ requestBody: ev.requestBody,
744
+ responseBody: ev.responseBody,
745
+ requestHeaders: ev.requestHeaders,
746
+ responseHeaders: ev.responseHeaders,
747
+ error: ev.error,
748
+ environment: this.defaults.environment,
749
+ release: this.defaults.release,
750
+ dist: this.defaults.dist,
751
+ platform: this.defaults.platform,
752
+ "sdk.name": this.defaults.sdkName,
753
+ "sdk.version": this.defaults.sdkVersion,
754
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
755
+ };
756
+ this.queue.push(item);
757
+ const isFailed = item.statusCode >= 400 || !!item.error;
758
+ if (isFailed) {
759
+ this.recentFailed.push(item);
760
+ if (this.recentFailed.length > RECENT_FAILED_BUFFER_SIZE) this.recentFailed.shift();
761
+ }
762
+ if (this.queue.length >= BATCH_SIZE_THRESHOLD2) {
763
+ this.flush();
764
+ return;
765
+ }
766
+ if (!this.flushTimer) this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS3);
767
+ }
768
+ /** Snapshot of the last failed requests for error-linking. Newest last. */
769
+ getRecentFailed() {
770
+ return this.recentFailed;
771
+ }
772
+ flush() {
773
+ if (this.queue.length === 0) return;
774
+ const batch = this.queue;
775
+ this.queue = [];
776
+ const payload = { requests: batch };
777
+ this.transport.send(INGEST_PATH, payload);
778
+ }
779
+ destroy() {
780
+ this.destroyed = true;
781
+ if (this.flushTimer) {
782
+ clearInterval(this.flushTimer);
783
+ this.flushTimer = null;
784
+ }
785
+ this.flush();
786
+ this.recentFailed = [];
787
+ }
788
+ /** @internal — for tests. */
789
+ getQueueSize() {
790
+ return this.queue.length;
791
+ }
792
+ };
793
+
794
+ // src/http-redact.ts
795
+ var ALWAYS_REDACT_HEADERS = /* @__PURE__ */ new Set([
796
+ "authorization",
797
+ "cookie",
798
+ "set-cookie",
799
+ "x-api-key",
800
+ "x-auth-token",
801
+ "proxy-authorization"
802
+ ]);
803
+ var ALWAYS_REDACT_QUERY = /* @__PURE__ */ new Set([
804
+ "token",
805
+ "password",
806
+ "api_key",
807
+ "apikey",
808
+ "authorization",
809
+ "auth",
810
+ "secret",
811
+ "access_token",
812
+ "refresh_token",
813
+ "session",
814
+ "sessionid",
815
+ "jwt"
816
+ ]);
817
+ var REDACTED = "[REDACTED]";
818
+ function shouldCaptureUrl(url, opts) {
819
+ if (!url) return false;
820
+ const lower = url.toLowerCase();
821
+ if (opts.allowedUrls && opts.allowedUrls.length > 0) {
822
+ return opts.allowedUrls.some((p) => matches(p, url, lower));
823
+ }
824
+ if (opts.ignoredUrls && opts.ignoredUrls.length > 0) {
825
+ return !opts.ignoredUrls.some((p) => matches(p, url, lower));
826
+ }
827
+ return true;
828
+ }
829
+ function matches(pattern, url, lower) {
830
+ if (pattern instanceof RegExp) return pattern.test(url);
831
+ if (typeof pattern !== "string") return false;
832
+ return lower.includes(pattern.toLowerCase());
833
+ }
834
+ function redactUrl(url, opts) {
835
+ if (!url) return url;
836
+ const extra = (opts.redactQueryParams ?? []).map((s) => s.toLowerCase());
837
+ const redactSet = /* @__PURE__ */ new Set([...ALWAYS_REDACT_QUERY, ...extra]);
838
+ let parsed = null;
839
+ try {
840
+ parsed = new URL(url);
841
+ } catch {
842
+ }
843
+ if (parsed) {
844
+ const params = parsed.searchParams;
845
+ let mutated = false;
846
+ const keysToRedact = [];
847
+ params.forEach((_v, k) => {
848
+ if (redactSet.has(k.toLowerCase())) keysToRedact.push(k);
849
+ });
850
+ for (const k of keysToRedact) {
851
+ params.set(k, REDACTED);
852
+ mutated = true;
853
+ }
854
+ if (mutated) parsed.search = params.toString();
855
+ return parsed.toString();
856
+ }
857
+ const qIdx = url.indexOf("?");
858
+ if (qIdx < 0) return url;
859
+ const head = url.slice(0, qIdx);
860
+ const queryAndHash = url.slice(qIdx + 1);
861
+ const hashIdx = queryAndHash.indexOf("#");
862
+ const query = hashIdx < 0 ? queryAndHash : queryAndHash.slice(0, hashIdx);
863
+ const hash = hashIdx < 0 ? "" : queryAndHash.slice(hashIdx);
864
+ const parts = query.split("&").map((pair) => {
865
+ const eq = pair.indexOf("=");
866
+ if (eq < 0) return pair;
867
+ const key = pair.slice(0, eq);
868
+ return redactSet.has(key.toLowerCase()) ? `${key}=${REDACTED}` : pair;
869
+ });
870
+ return `${head}?${parts.join("&")}${hash ? "#" + hash : ""}`;
871
+ }
872
+ function sanitizeHeaders(headers, opts) {
873
+ if (!opts.captureHeaders) return void 0;
874
+ if (!headers) return {};
875
+ const extra = (opts.redactHeaders ?? []).map((s) => s.toLowerCase());
876
+ const redactSet = /* @__PURE__ */ new Set([...ALWAYS_REDACT_HEADERS, ...extra]);
877
+ const out = {};
878
+ for (const [k, v] of Object.entries(headers)) {
879
+ if (v == null) continue;
880
+ const lower = k.toLowerCase();
881
+ out[lower] = redactSet.has(lower) ? REDACTED : Array.isArray(v) ? v.join(", ") : String(v);
882
+ }
883
+ return out;
884
+ }
885
+ function captureBody(body, enabled, maxBodyBytes) {
886
+ if (!enabled) return void 0;
887
+ if (body == null) return void 0;
888
+ let str;
889
+ if (typeof body === "string") str = body;
890
+ else if (typeof body === "number" || typeof body === "boolean") str = String(body);
891
+ else if (typeof body === "object") {
892
+ const tag = Object.prototype.toString.call(body);
893
+ if (tag !== "[object Object]" && tag !== "[object Array]") return "<binary>";
894
+ try {
895
+ str = JSON.stringify(body);
896
+ } catch {
897
+ return "<unserializable>";
898
+ }
899
+ } else {
900
+ return "<binary>";
901
+ }
902
+ if (str.length > maxBodyBytes) return str.slice(0, maxBodyBytes) + "\u2026[truncated]";
903
+ return str;
904
+ }
905
+
906
+ // src/http-instrumentation.ts
907
+ var FETCH_FLAG2 = "__allstak_http_fetch_patched__";
908
+ var XHR_FLAG = "__allstak_http_xhr_patched__";
909
+ var AXIOS_FLAG = /* @__PURE__ */ Symbol.for("allstak.http.axios.instrumented");
910
+ var DEFAULT_MAX_BODY = 4096;
911
+ var _currentModule = null;
912
+ var _currentOpts = null;
913
+ function currentModule() {
914
+ return _currentModule;
915
+ }
916
+ function currentOpts() {
917
+ return _currentOpts;
918
+ }
919
+ function safeCapture(ev) {
920
+ try {
921
+ currentModule()?.capture(ev);
922
+ } catch {
923
+ }
924
+ }
925
+ function bind(opts, ownIngestHost) {
926
+ return {
927
+ captureRequestBody: opts.captureRequestBody ?? false,
928
+ captureResponseBody: opts.captureResponseBody ?? false,
929
+ captureHeaders: opts.captureHeaders ?? false,
930
+ redactHeaders: opts.redactHeaders ?? [],
931
+ redactQueryParams: opts.redactQueryParams ?? [],
932
+ ignoredUrls: opts.ignoredUrls ?? [],
933
+ allowedUrls: opts.allowedUrls ?? [],
934
+ maxBodyBytes: opts.maxBodyBytes ?? DEFAULT_MAX_BODY,
935
+ ownIngestPrefix: ownIngestHost.replace(/\/$/, "")
936
+ };
937
+ }
938
+ function isOwnIngest(url, prefix) {
939
+ return !!prefix && url.startsWith(prefix);
940
+ }
941
+ function urlString(input) {
942
+ if (typeof input === "string") return input;
943
+ if (input && typeof input.href === "string") return input.href;
944
+ if (input && typeof input.url === "string") return input.url;
945
+ return String(input);
946
+ }
947
+ function safeByteLength(s) {
948
+ if (s == null) return void 0;
949
+ if (typeof s === "string") {
950
+ let n = 0;
951
+ for (let i = 0; i < s.length; i++) {
952
+ const c = s.charCodeAt(i);
953
+ if (c < 128) n += 1;
954
+ else if (c < 2048) n += 2;
955
+ else if (c >= 55296 && c <= 56319) {
956
+ n += 4;
957
+ i += 1;
958
+ } else n += 3;
959
+ }
960
+ return n;
961
+ }
962
+ return void 0;
963
+ }
964
+ function headersToObject(h) {
965
+ if (!h) return {};
966
+ if (typeof h.entries === "function") {
967
+ const out = {};
968
+ for (const [k, v] of h.entries()) out[String(k).toLowerCase()] = String(v);
969
+ return out;
970
+ }
971
+ if (typeof h === "object") {
972
+ const out = {};
973
+ for (const [k, v] of Object.entries(h)) {
974
+ if (v == null) continue;
975
+ out[String(k).toLowerCase()] = Array.isArray(v) ? v.join(", ") : String(v);
976
+ }
977
+ return out;
978
+ }
979
+ return {};
980
+ }
981
+ function patchFetch() {
982
+ const g = globalThis;
983
+ if (typeof g.fetch !== "function") return;
984
+ if (g.fetch[FETCH_FLAG2]) return;
985
+ const original = g.fetch;
986
+ const wrapped = async function(input, init) {
987
+ const opts = currentOpts();
988
+ if (!opts || !currentModule()) {
989
+ return original.call(this, input, init);
990
+ }
991
+ const rawUrl = urlString(input);
992
+ const sanitizedUrl = redactUrl(rawUrl, opts);
993
+ const method = (init?.method || input && typeof input === "object" && input.method || "GET").toUpperCase();
994
+ if (isOwnIngest(rawUrl, opts.ownIngestPrefix) || !shouldCaptureUrl(rawUrl, opts)) {
995
+ return original.call(this, input, init);
996
+ }
997
+ const start = Date.now();
998
+ const reqHeaders = sanitizeHeaders(headersToObject(init?.headers ?? (input && input.headers)), opts);
999
+ const reqBody = captureBody(init?.body, opts.captureRequestBody, opts.maxBodyBytes);
1000
+ const reqSize = safeByteLength(typeof init?.body === "string" ? init.body : void 0);
1001
+ let response;
1002
+ try {
1003
+ response = await original.call(this, input, init);
1004
+ } catch (err) {
1005
+ safeCapture({
1006
+ type: "http_request",
1007
+ method,
1008
+ url: sanitizedUrl,
1009
+ statusCode: 0,
1010
+ durationMs: Date.now() - start,
1011
+ requestBody: reqBody,
1012
+ requestHeaders: reqHeaders,
1013
+ requestSize: reqSize,
1014
+ error: String(err?.message ?? err)
1015
+ });
1016
+ throw err;
1017
+ }
1018
+ const durationMs = Date.now() - start;
1019
+ let respBody;
1020
+ let respSize;
1021
+ let respHeaders;
1022
+ try {
1023
+ respHeaders = sanitizeHeaders(headersToObject(response.headers), opts);
1024
+ const lenHeader = typeof response.headers?.get === "function" ? response.headers.get("content-length") : null;
1025
+ if (lenHeader) respSize = parseInt(lenHeader, 10) || void 0;
1026
+ if (opts.captureResponseBody && typeof response.clone === "function") {
1027
+ try {
1028
+ const cloned = response.clone();
1029
+ const text = await cloned.text();
1030
+ respBody = captureBody(text, true, opts.maxBodyBytes);
1031
+ if (respSize == null) respSize = safeByteLength(text);
1032
+ } catch {
1033
+ }
1034
+ }
1035
+ } catch {
1036
+ }
1037
+ safeCapture({
1038
+ type: "http_request",
1039
+ method,
1040
+ url: sanitizedUrl,
1041
+ statusCode: response.status,
1042
+ durationMs,
1043
+ requestBody: reqBody,
1044
+ requestHeaders: reqHeaders,
1045
+ requestSize: reqSize,
1046
+ responseBody: respBody,
1047
+ responseHeaders: respHeaders,
1048
+ responseSize: respSize
1049
+ });
1050
+ return response;
1051
+ };
1052
+ wrapped[FETCH_FLAG2] = true;
1053
+ g.fetch = wrapped;
1054
+ }
1055
+ function patchXhr() {
1056
+ const g = globalThis;
1057
+ const X = g.XMLHttpRequest;
1058
+ if (!X || X.prototype[XHR_FLAG]) return;
1059
+ const origOpen = X.prototype.open;
1060
+ const origSend = X.prototype.send;
1061
+ const origSetRequestHeader = X.prototype.setRequestHeader;
1062
+ X.prototype.open = function(method, url, ...rest) {
1063
+ this.__allstak_method__ = method;
1064
+ this.__allstak_url__ = url;
1065
+ this.__allstak_headers__ = {};
1066
+ return origOpen.call(this, method, url, ...rest);
1067
+ };
1068
+ X.prototype.setRequestHeader = function(name, value) {
1069
+ try {
1070
+ this.__allstak_headers__[String(name).toLowerCase()] = String(value);
1071
+ } catch {
1072
+ }
1073
+ return origSetRequestHeader.call(this, name, value);
1074
+ };
1075
+ X.prototype.send = function(body) {
1076
+ const opts = currentOpts();
1077
+ if (!opts || !currentModule()) return origSend.call(this, body);
1078
+ const start = Date.now();
1079
+ const method = String(this.__allstak_method__ || "GET").toUpperCase();
1080
+ const rawUrl = String(this.__allstak_url__ || "");
1081
+ const sanitizedUrl = redactUrl(rawUrl, opts);
1082
+ if (isOwnIngest(rawUrl, opts.ownIngestPrefix) || !shouldCaptureUrl(rawUrl, opts)) {
1083
+ return origSend.call(this, body);
1084
+ }
1085
+ const reqHeaders = sanitizeHeaders(this.__allstak_headers__, opts);
1086
+ const reqBody = captureBody(body, opts.captureRequestBody, opts.maxBodyBytes);
1087
+ const reqSize = safeByteLength(typeof body === "string" ? body : void 0);
1088
+ const finish = (statusCode, error) => {
1089
+ const durationMs = Date.now() - start;
1090
+ let respHeaders;
1091
+ let respBody;
1092
+ let respSize;
1093
+ try {
1094
+ const liveOpts = currentOpts() ?? opts;
1095
+ if (liveOpts.captureHeaders && typeof this.getAllResponseHeaders === "function") {
1096
+ const raw = this.getAllResponseHeaders() || "";
1097
+ const dict = {};
1098
+ for (const line of raw.split(/\r?\n/)) {
1099
+ const idx = line.indexOf(":");
1100
+ if (idx > 0) dict[line.slice(0, idx).trim().toLowerCase()] = line.slice(idx + 1).trim();
1101
+ }
1102
+ respHeaders = sanitizeHeaders(dict, liveOpts);
1103
+ }
1104
+ if (liveOpts.captureResponseBody) {
1105
+ const text = this.responseText;
1106
+ if (typeof text === "string") {
1107
+ respBody = captureBody(text, true, liveOpts.maxBodyBytes);
1108
+ respSize = safeByteLength(text);
1109
+ }
1110
+ }
1111
+ } catch {
1112
+ }
1113
+ safeCapture({
1114
+ type: "http_request",
1115
+ method,
1116
+ url: sanitizedUrl,
1117
+ statusCode,
1118
+ durationMs,
1119
+ requestBody: reqBody,
1120
+ requestHeaders: reqHeaders,
1121
+ requestSize: reqSize,
1122
+ responseBody: respBody,
1123
+ responseHeaders: respHeaders,
1124
+ responseSize: respSize,
1125
+ error
1126
+ });
1127
+ };
1128
+ this.addEventListener?.("load", () => finish(this.status || 0));
1129
+ this.addEventListener?.("error", () => finish(0, "network"));
1130
+ this.addEventListener?.("abort", () => finish(0, "abort"));
1131
+ this.addEventListener?.("timeout", () => finish(0, "timeout"));
1132
+ return origSend.call(this, body);
1133
+ };
1134
+ X.prototype[XHR_FLAG] = true;
1135
+ }
1136
+ function instrumentAxiosInstance(axiosInstance, module, opts) {
1137
+ if (!axiosInstance || typeof axiosInstance.interceptors !== "object") return axiosInstance;
1138
+ if (axiosInstance[AXIOS_FLAG]) return axiosInstance;
1139
+ axiosInstance[AXIOS_FLAG] = true;
1140
+ const reqStarts = /* @__PURE__ */ new WeakMap();
1141
+ axiosInstance.interceptors.request.use((config) => {
1142
+ try {
1143
+ const rawUrl = (config.baseURL ? config.baseURL.replace(/\/$/, "") : "") + (config.url || "");
1144
+ reqStarts.set(config, {
1145
+ start: Date.now(),
1146
+ method: String(config.method || "GET").toUpperCase(),
1147
+ rawUrl
1148
+ });
1149
+ } catch {
1150
+ }
1151
+ return config;
1152
+ });
1153
+ const finalize = (cfg, statusCode, response, error) => {
1154
+ const meta = reqStarts.get(cfg);
1155
+ if (!meta) return;
1156
+ reqStarts.delete(cfg);
1157
+ if (isOwnIngest(meta.rawUrl, opts.ownIngestPrefix)) return;
1158
+ if (!shouldCaptureUrl(meta.rawUrl, opts)) return;
1159
+ const sanitizedUrl = redactUrl(meta.rawUrl, opts);
1160
+ const reqHeaders = sanitizeHeaders(headersToObject(cfg.headers), opts);
1161
+ const reqBody = captureBody(cfg.data, opts.captureRequestBody, opts.maxBodyBytes);
1162
+ const respHeaders = sanitizeHeaders(headersToObject(response?.headers), opts);
1163
+ const respBody = captureBody(response?.data, opts.captureResponseBody, opts.maxBodyBytes);
1164
+ try {
1165
+ module.capture({
1166
+ type: "http_request",
1167
+ method: meta.method,
1168
+ url: sanitizedUrl,
1169
+ statusCode,
1170
+ durationMs: Date.now() - meta.start,
1171
+ requestBody: reqBody,
1172
+ requestHeaders: reqHeaders,
1173
+ responseBody: respBody,
1174
+ responseHeaders: respHeaders,
1175
+ error
1176
+ });
1177
+ } catch {
1178
+ }
1179
+ };
1180
+ axiosInstance.interceptors.response.use(
1181
+ (response) => {
1182
+ finalize(response.config, response.status);
1183
+ return response;
1184
+ },
1185
+ (err) => {
1186
+ const cfg = err?.config;
1187
+ const status = err?.response?.status ?? 0;
1188
+ finalize(cfg, status, err?.response, String(err?.message ?? err));
1189
+ throw err;
1190
+ }
1191
+ );
1192
+ return axiosInstance;
1193
+ }
1194
+ function tryAutoInstrumentAxios(module, opts) {
1195
+ try {
1196
+ const axios = globalThis.require?.("axios") ?? null;
1197
+ if (axios) instrumentAxiosInstance(axios.default ?? axios, module, opts);
1198
+ } catch {
1199
+ }
1200
+ }
1201
+ function installHttpInstrumentation(module, options, ownIngestHost) {
1202
+ const bound = bind(options, ownIngestHost);
1203
+ _currentModule = module;
1204
+ _currentOpts = bound;
1205
+ try {
1206
+ patchFetch();
1207
+ } catch {
1208
+ }
1209
+ try {
1210
+ patchXhr();
1211
+ } catch {
1212
+ }
1213
+ try {
1214
+ tryAutoInstrumentAxios(module, bound);
1215
+ } catch {
1216
+ }
1217
+ return {
1218
+ instrumentAxios: (axios) => instrumentAxiosInstance(axios, module, bound)
1219
+ };
1220
+ }
1221
+
698
1222
  // src/client.ts
699
1223
  var INGEST_HOST = "https://api.allstak.sa";
700
1224
  var SDK_NAME = "allstak-react";
701
- var SDK_VERSION = "0.2.1";
1225
+ var SDK_VERSION = "0.3.0";
702
1226
  var ERRORS_PATH = "/ingest/v1/errors";
703
1227
  var LOGS_PATH = "/ingest/v1/logs";
704
1228
  var VALID_BREADCRUMB_TYPES = /* @__PURE__ */ new Set(["http", "log", "ui", "navigation", "query", "default"]);
@@ -728,6 +1252,8 @@ var AllStakClient = class {
728
1252
  this.breadcrumbs = [];
729
1253
  this.scopeStack = [];
730
1254
  this.replay = null;
1255
+ this.httpRequests = null;
1256
+ this._instrumentAxios = null;
731
1257
  this.onErrorHandler = null;
732
1258
  this.onRejectionHandler = null;
733
1259
  if (!config.apiKey) throw new Error("AllStak: config.apiKey is required");
@@ -773,6 +1299,35 @@ var AllStakClient = class {
773
1299
  } catch {
774
1300
  }
775
1301
  }
1302
+ if (config.enableHttpTracking) {
1303
+ try {
1304
+ this.httpRequests = new HttpRequestModule(this.transport);
1305
+ this.httpRequests.setDefaults({
1306
+ environment: this.config.environment,
1307
+ release: this.config.release,
1308
+ dist: this.config.dist,
1309
+ platform: this.config.platform,
1310
+ sdkName: this.config.sdkName,
1311
+ sdkVersion: this.config.sdkVersion
1312
+ });
1313
+ const { instrumentAxios } = installHttpInstrumentation(
1314
+ this.httpRequests,
1315
+ config.httpTracking ?? {},
1316
+ baseUrl
1317
+ );
1318
+ this._instrumentAxios = instrumentAxios;
1319
+ } catch {
1320
+ }
1321
+ }
1322
+ }
1323
+ /** Manually instrument an axios instance. No-op when HTTP tracking is off. */
1324
+ instrumentAxios(axios) {
1325
+ if (!this._instrumentAxios) return axios;
1326
+ return this._instrumentAxios(axios);
1327
+ }
1328
+ /** Snapshot of recent failed HTTP requests for error-linking. */
1329
+ getRecentFailedHttp() {
1330
+ return this.httpRequests?.getRecentFailed() ?? [];
776
1331
  }
777
1332
  captureException(error, context) {
778
1333
  if (!this.passesSampleRate()) return;
@@ -791,6 +1346,16 @@ var AllStakClient = class {
791
1346
  if (traceId) traceContext.traceId = traceId;
792
1347
  const spanId = this.tracing.getCurrentSpanId();
793
1348
  if (spanId) traceContext.spanId = spanId;
1349
+ const recentFailed = this.httpRequests?.getRecentFailed() ?? [];
1350
+ if (recentFailed.length > 0) {
1351
+ traceContext["http.recentFailed"] = recentFailed.map((r) => ({
1352
+ method: r.method,
1353
+ url: r.url,
1354
+ statusCode: r.statusCode,
1355
+ durationMs: r.durationMs,
1356
+ error: r.error
1357
+ }));
1358
+ }
794
1359
  const payload = {
795
1360
  exceptionClass,
796
1361
  message: error.message,
@@ -947,6 +1512,11 @@ var AllStakClient = class {
947
1512
  this.replay.destroy();
948
1513
  this.replay = null;
949
1514
  }
1515
+ if (this.httpRequests) {
1516
+ this.httpRequests.destroy();
1517
+ this.httpRequests = null;
1518
+ }
1519
+ this._instrumentAxios = null;
950
1520
  this.breadcrumbs = [];
951
1521
  }
952
1522
  installBrowserHandlers() {
@@ -1155,6 +1725,10 @@ var AllStak = {
1155
1725
  resetTrace() {
1156
1726
  ensureInit().resetTrace();
1157
1727
  },
1728
+ /** Manually instrument an axios instance. No-op when HTTP tracking is off. */
1729
+ instrumentAxios(axios) {
1730
+ return ensureInit().instrumentAxios(axios);
1731
+ },
1158
1732
  getSessionId() {
1159
1733
  return ensureInit().getSessionId();
1160
1734
  },
@@ -1242,6 +1816,7 @@ export {
1242
1816
  AllStak,
1243
1817
  AllStakClient,
1244
1818
  AllStakErrorBoundary,
1819
+ HttpRequestModule,
1245
1820
  INGEST_HOST,
1246
1821
  ReplayRecorder,
1247
1822
  SDK_NAME,