@safaricom-mxl/effect-sdk 0.0.22 → 0.0.24

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.
@@ -0,0 +1,1068 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D8jQpDG7.js";
2
+ import { Cause, Context, Effect, Exit, Layer, Option, Predicate, Tracer } from "effect";
3
+ import * as ErrorReporter from "effect/ErrorReporter";
4
+ import * as OtlpResource from "effect/observability/OtlpResource";
5
+ import * as WebSdk from "@effect/opentelemetry/WebSdk";
6
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
7
+ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
8
+ // src/shared/url-privacy.ts
9
+ const REDACTED = "REDACTED";
10
+ const SENSITIVE_PARAM = /^(access_token|id_token|refresh_token|token|token_hash|auth|authorization|code|oobcode|state|password|passwd|pwd|pass|secret|client_secret|api_key|apikey|key|signature|sig|otp|jwt|session|sessionid|session_id|ticket|__clerk_ticket|reset_token|magic|nonce|x-amz-signature|x-amz-credential|x-amz-security-token|x-goog-signature|x-goog-credential)$/i;
11
+ /** Sanitizers live on `globalThis`, like consent: one page, one policy, however many SDK copies. */
12
+ const SANITIZERS_KEY = "__MXL_URL_SANITIZERS__";
13
+ function sanitizers() {
14
+ const owner = globalThis;
15
+ const existing = owner[SANITIZERS_KEY];
16
+ if (existing instanceof Set) return existing;
17
+ const fresh = /* @__PURE__ */ new Set();
18
+ owner[SANITIZERS_KEY] = fresh;
19
+ return fresh;
20
+ }
21
+ /**
22
+ * Register a host-app sanitizer. Additive, like the consent gates: an SDK
23
+ * initialized without one must not remove the one another SDK was given.
24
+ */
25
+ function addUrlSanitizer(sanitizer) {
26
+ sanitizers().add(sanitizer);
27
+ }
28
+ /** Test seam. */
29
+ function resetUrlSanitizersForTests() {
30
+ sanitizers().clear();
31
+ }
32
+ function redactParams(params) {
33
+ let changed = false;
34
+ for (const name of new Set(params.keys())) {
35
+ if (!SENSITIVE_PARAM.test(name)) continue;
36
+ params.set(name, REDACTED);
37
+ changed = true;
38
+ }
39
+ return changed;
40
+ }
41
+ /**
42
+ * Implicit-flow tokens ride the fragment as `#access_token=…&…`; a hash-routed
43
+ * app carries them in the route's own query, `#/reset?token=…`. The route path
44
+ * is kept either way.
45
+ */
46
+ function redactFragment(fragment) {
47
+ if (fragment.startsWith("/") || fragment.startsWith("!/")) {
48
+ const queryAt = fragment.indexOf("?");
49
+ if (queryAt === -1) return fragment;
50
+ const params = new URLSearchParams(fragment.slice(queryAt + 1));
51
+ return redactParams(params) ? `${fragment.slice(0, queryAt)}?${params.toString()}` : fragment;
52
+ }
53
+ if (!fragment.includes("=")) return fragment;
54
+ const params = new URLSearchParams(fragment);
55
+ return redactParams(params) ? params.toString() : fragment;
56
+ }
57
+ const ABSOLUTE = /^[a-z][a-z0-9+.-]*:/i;
58
+ const RELATIVE_BASE = "http://relative.invalid";
59
+ /** Redact credential-shaped query and fragment parameters, keeping the URL's form. */
60
+ function redactUrl(url) {
61
+ if (!url || !url.includes("?") && !url.includes("#")) return url;
62
+ const absolute = ABSOLUTE.test(url);
63
+ let parsed;
64
+ try {
65
+ parsed = new URL(url, absolute ? void 0 : RELATIVE_BASE);
66
+ } catch {
67
+ return url;
68
+ }
69
+ let changed = redactParams(parsed.searchParams);
70
+ const fragment = parsed.hash.slice(1);
71
+ const redactedFragment = redactFragment(fragment);
72
+ if (redactedFragment !== fragment) {
73
+ parsed.hash = redactedFragment;
74
+ changed = true;
75
+ }
76
+ if (!changed) return url;
77
+ if (absolute) return parsed.href;
78
+ if (url.startsWith("//")) return parsed.href.slice(parsed.protocol.length);
79
+ const tail = `${parsed.pathname}${parsed.search}${parsed.hash}`;
80
+ return url.startsWith("/") ? tail : tail.replace(/^\//, "");
81
+ }
82
+ /**
83
+ * The URL as it may leave the page: default redaction, then every registered
84
+ * host sanitizer. A sanitizer that throws or returns a non-string yields the
85
+ * default-redacted URL rather than the raw one.
86
+ */
87
+ function scrubUrl(url) {
88
+ let out = redactUrl(url);
89
+ for (const sanitizer of sanitizers()) try {
90
+ const next = sanitizer(out);
91
+ if (typeof next === "string") out = next;
92
+ } catch {}
93
+ return out;
94
+ }
95
+ // src/shared/identity.ts
96
+ const normalizeIdentity = (input) => {
97
+ if (input === null || input === void 0) return;
98
+ if (typeof input === "string") {
99
+ const id = input.trim();
100
+ return id.length > 0 ? { id } : void 0;
101
+ }
102
+ const id = input.id.trim();
103
+ if (id.length === 0) return;
104
+ return {
105
+ email: input.email,
106
+ groupId: input.groupId,
107
+ groupName: input.groupName,
108
+ id,
109
+ traits: input.traits
110
+ };
111
+ };
112
+ const RESERVED_EVENT_NAMES = ["session_start", "session_end"];
113
+ const isReservedEventName = (name) => RESERVED_EVENT_NAMES.some((reserved) => reserved === name);
114
+ const coerceTrackProps = (props) => {
115
+ if (props === void 0) return {};
116
+ const attributes = {};
117
+ for (const [key, value] of Object.entries(props)) {
118
+ if (value === void 0) continue;
119
+ attributes[key] = typeof value === "string" ? value : String(value);
120
+ }
121
+ return attributes;
122
+ };
123
+ const toEventTimestamp = (value) => (value instanceof Date ? value : new Date(value ?? Date.now())).toISOString();
124
+ const encodeProductEventLine = (input) => {
125
+ const trimmed = typeof input.name === "string" ? input.name.trim() : "";
126
+ if (trimmed.length === 0 || isReservedEventName(trimmed)) return;
127
+ const options = input.options ?? {};
128
+ return {
129
+ attributes: coerceTrackProps(options.attributes),
130
+ group_id: options.groupId ?? "",
131
+ name: trimmed.slice(0, 80),
132
+ page_path: options.pagePath ?? "",
133
+ service_name: input.serviceName,
134
+ session_id: options.sessionId ?? "",
135
+ source: input.source,
136
+ timestamp: toEventTimestamp(options.timestamp),
137
+ traceparent: options.traceparent,
138
+ url: options.url ?? "",
139
+ user_id: options.userId ?? "",
140
+ visitor_id: options.visitorId ?? ""
141
+ };
142
+ };
143
+ const encodeNdjson = (lines) => `${lines.map((line) => JSON.stringify(line)).join("\n")}\n`;
144
+ // src/version.ts
145
+ const SDK_NAME = "mxl-effect-sdk";
146
+ const SDK_VERSION = "0.0.24";
147
+ // src/browser/analytics.ts
148
+ const DEFAULT_INGEST_URL = "http://127.0.0.1:8000";
149
+ const MAX_QUEUE = 100;
150
+ let config;
151
+ let identity;
152
+ let consentGranted = true;
153
+ let sessionId = "";
154
+ let queue = [];
155
+ let started = false;
156
+ const randomId = () => {
157
+ if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();
158
+ return `mxl_${Math.random().toString(36).slice(2)}_${Date.now()}`;
159
+ };
160
+ const storageGet = (key) => {
161
+ try {
162
+ const value = globalThis.sessionStorage?.getItem(key);
163
+ return value && value.length > 0 ? value : void 0;
164
+ } catch {
165
+ return;
166
+ }
167
+ };
168
+ const storageSet = (key, value) => {
169
+ try {
170
+ globalThis.sessionStorage?.setItem(key, value);
171
+ } catch {}
172
+ };
173
+ const resolveSessionId = () => {
174
+ if (sessionId.length > 0) return sessionId;
175
+ sessionId = storageGet("mxl.session.id") ?? randomId();
176
+ storageSet("mxl.session.id", sessionId);
177
+ return sessionId;
178
+ };
179
+ const ingestOrigin = () => (config?.ingestUrl ?? DEFAULT_INGEST_URL).replace(/\/$/, "");
180
+ const headers = () => {
181
+ const next = {
182
+ "content-type": "application/json",
183
+ "mxl-client-id": config?.clientId ?? "",
184
+ "mxl-sdk-name": SDK_NAME,
185
+ "mxl-sdk-version": SDK_VERSION
186
+ };
187
+ if (config?.apiKey) next["mxl-api-key"] = config.apiKey;
188
+ return next;
189
+ };
190
+ const post = (path, body, contentType, keepalive = false) => {
191
+ if (!config?.clientId) return;
192
+ try {
193
+ fetch(`${ingestOrigin()}${path}`, {
194
+ body,
195
+ headers: {
196
+ ...headers(),
197
+ "content-type": contentType
198
+ },
199
+ keepalive,
200
+ method: "POST"
201
+ });
202
+ } catch {}
203
+ };
204
+ const captureAllowed = () => Boolean(config?.clientId) && consentGranted && started;
205
+ const getCurrentIdentity = () => identity;
206
+ const getCurrentUserId = () => identity?.id;
207
+ const getCurrentSessionId = () => resolveSessionId();
208
+ const configureBrowserAnalytics = (next) => {
209
+ config = next;
210
+ consentGranted = next.privacy?.requireConsent !== true;
211
+ started = true;
212
+ resolveSessionId();
213
+ drainQueue();
214
+ };
215
+ const setConsent = (granted) => {
216
+ consentGranted = granted;
217
+ if (granted) drainQueue();
218
+ };
219
+ const identify$1 = (input) => {
220
+ identity = normalizeIdentity(input);
221
+ if (!captureAllowed() || identity === void 0) return;
222
+ post("/track", JSON.stringify({
223
+ payload: {
224
+ email: identity.email,
225
+ firstName: typeof identity.traits?.["firstName"] === "string" ? identity.traits["firstName"] : void 0,
226
+ lastName: typeof identity.traits?.["lastName"] === "string" ? identity.traits["lastName"] : void 0,
227
+ profileId: identity.id,
228
+ properties: {
229
+ ...identity.traits,
230
+ groupId: identity.groupId,
231
+ groupName: identity.groupName
232
+ }
233
+ },
234
+ type: "identify"
235
+ }), "application/json");
236
+ };
237
+ const clearIdentity$1 = () => {
238
+ identity = void 0;
239
+ };
240
+ const postProductAndSession = (name, props, keepalive = false) => {
241
+ const line = encodeProductEventLine({
242
+ name,
243
+ options: {
244
+ attributes: props,
245
+ groupId: identity?.groupId,
246
+ sessionId: resolveSessionId(),
247
+ userId: identity?.id
248
+ },
249
+ serviceName: config?.serviceName ?? "unknown",
250
+ source: "browser"
251
+ });
252
+ if (line === void 0) return;
253
+ post("/events", encodeNdjson([line]), "application/x-ndjson", keepalive);
254
+ post("/sessionEvents", `${JSON.stringify({
255
+ attributes: line.attributes,
256
+ message: line.name,
257
+ session_id: line.session_id,
258
+ timestamp: line.timestamp,
259
+ type: "custom"
260
+ })}\n`, "application/x-ndjson", keepalive);
261
+ };
262
+ const drainQueue = (keepalive = false) => {
263
+ if (!captureAllowed()) return;
264
+ const pending = queue;
265
+ queue = [];
266
+ for (const event of pending) postProductAndSession(event.name, event.props, keepalive);
267
+ };
268
+ const track$1 = (name, props) => {
269
+ if (!started || !consentGranted || !config?.clientId) {
270
+ if (queue.length >= MAX_QUEUE) queue.shift();
271
+ queue.push({
272
+ name,
273
+ props
274
+ });
275
+ return;
276
+ }
277
+ postProductAndSession(name, props);
278
+ };
279
+ const postProfileDelta = (type, property, value) => {
280
+ if (!captureAllowed() || identity === void 0) return;
281
+ post("/track", JSON.stringify({
282
+ payload: {
283
+ profileId: identity.id,
284
+ property,
285
+ value: value ?? 1
286
+ },
287
+ type
288
+ }), "application/json");
289
+ };
290
+ const increment = (property, value) => {
291
+ postProfileDelta("increment", property, value);
292
+ };
293
+ const decrement = (property, value) => {
294
+ postProfileDelta("decrement", property, value);
295
+ };
296
+ const flushAnalytics = (keepalive = false) => {
297
+ drainQueue(keepalive);
298
+ };
299
+ // src/shared/try-sync.ts
300
+ /** Run a throwing synchronous call, `None` if it threw. */
301
+ const trySync = (thunk) => Effect.runSync(Effect.option(Effect.try(thunk)));
302
+ /** Run a throwing synchronous call, `undefined` if it threw. */
303
+ const trySyncOrUndefined = (thunk) => Option.getOrUndefined(trySync(thunk));
304
+ // src/browser/browser-globals.ts
305
+ /**
306
+ * Narrow optional globals for the DOM-free SDK build, which also runs in
307
+ * Workers and Node.
308
+ */
309
+ const browserNavigator = () => globalThis.navigator;
310
+ const browserDocument = () => globalThis.document;
311
+ /** Resource attributes every browser preset sends: the SDK, the user agent, language and time zone. */
312
+ const browserResourceAttributes = () => {
313
+ const nav = browserNavigator();
314
+ const timezone = typeof Intl === "undefined" ? void 0 : trySyncOrUndefined(() => Intl.DateTimeFormat().resolvedOptions().timeZone);
315
+ return {
316
+ "telemetry.sdk.name": "@safaricom-mxl/effect-sdk",
317
+ "telemetry.sdk.language": "webjs",
318
+ "telemetry.sdk.version": SDK_VERSION,
319
+ ...nav?.userAgent ? { "user_agent.original": nav.userAgent } : void 0,
320
+ ...nav?.language ? { "browser.language": nav.language } : void 0,
321
+ ...timezone ? { "mxl.browser.timezone": timezone } : void 0
322
+ };
323
+ };
324
+ // src/server/otlp.ts
325
+ function normalizeOtlpBaseUrl(url) {
326
+ const trimmed = url.replace(/\/$/, "");
327
+ if (trimmed.endsWith("/v1/traces") || trimmed.endsWith("/v1/logs")) return trimmed.replace(/\/v1\/(traces|logs|metrics)$/, "");
328
+ return trimmed;
329
+ }
330
+ // src/shared/flush-core.ts
331
+ const COOLDOWN_MS = 6e4;
332
+ const makeSerializedFlush = (flush) => {
333
+ let pending = Promise.resolve();
334
+ return (...args) => {
335
+ pending = pending.then(() => flush(...args), () => flush(...args));
336
+ return pending;
337
+ };
338
+ };
339
+ const guardFlush = (prefix, flush, state) => {
340
+ return async (...args) => {
341
+ if (state && Date.now() < state.disabledUntil) return;
342
+ try {
343
+ await flush(...args);
344
+ } catch (error) {
345
+ if (state) state.disabledUntil = Date.now() + COOLDOWN_MS;
346
+ console.error(`${prefix} flush failed; cooling down 60s`, error);
347
+ }
348
+ };
349
+ };
350
+ const fetchTransport = async (input) => {
351
+ const body = typeof input.body === "string" ? input.body : new Uint8Array(input.body);
352
+ const response = await fetch(input.url, {
353
+ body,
354
+ headers: input.headers,
355
+ keepalive: input.keepalive,
356
+ method: "POST"
357
+ });
358
+ if (!response.ok) throw new Error(`POST ${input.url} → ${response.status}`);
359
+ };
360
+ // src/shared/span-options.ts
361
+ const prefixMatcher = (prefixes) => {
362
+ const list = prefixes === void 0 ? [] : [...prefixes];
363
+ return list.length > 0 ? (name) => list.some((prefix) => name.startsWith(prefix)) : void 0;
364
+ };
365
+ const failureIdentifier = (error) => {
366
+ if (Predicate.hasProperty(error, "_tag") && typeof error._tag === "string") return error._tag;
367
+ if (Predicate.hasProperty(error, "name") && typeof error.name === "string") return error.name;
368
+ const body = Predicate.hasProperty(error, "error") ? error.error : void 0;
369
+ if (Predicate.hasProperty(body, "_tag") && typeof body._tag === "string") return body._tag;
370
+ };
371
+ const resolveSpanFilter = (input = {}) => {
372
+ const dropName = prefixMatcher(input.dropSpanNames);
373
+ const dropPredicate = input.dropSpan;
374
+ const drop = dropName !== void 0 && dropPredicate !== void 0 ? (span) => dropName(span.name) || dropPredicate(span) : dropName !== void 0 ? (span) => dropName(span.name) : dropPredicate;
375
+ const identifiers = /* @__PURE__ */ new Set([...input.anticipatedErrorIdentifiers ?? [], ...input.anticipatedErrorTags ?? []]);
376
+ const byIdentifier = identifiers.size > 0 ? (error) => {
377
+ const identifier = failureIdentifier(error);
378
+ return identifier !== void 0 && identifiers.has(identifier);
379
+ } : void 0;
380
+ const byPredicate = input.isAnticipatedError;
381
+ const isAnticipated = byIdentifier !== void 0 && byPredicate !== void 0 ? (error) => byIdentifier(error) || byPredicate(error) : byIdentifier ?? byPredicate;
382
+ return {
383
+ dropSubtree: prefixMatcher(input.dropSpanSubtrees),
384
+ drop,
385
+ isAnticipated,
386
+ includeCauseInStack: input.includeCauseInStack ?? false
387
+ };
388
+ };
389
+ // src/shared/flushable-tracer.ts
390
+ const MAX_BUFFER = 1e4;
391
+ const isIgnoredFailure = (error, anticipated) => Predicate.hasProperty(error, ErrorReporter.ignore) && error[ErrorReporter.ignore] === true && !(anticipated !== void 0 && anticipated(error));
392
+ const isIgnoredExit = (exit, anticipated) => {
393
+ if (exit._tag !== "Failure") return false;
394
+ if (exit.cause.reasons.some(Cause.isDieReason)) return false;
395
+ const failures = exit.cause.reasons.filter(Cause.isFailReason);
396
+ return failures.length > 0 && failures.every((reason) => isIgnoredFailure(reason.error, anticipated));
397
+ };
398
+ /**
399
+ * Turns a finished span into its OTLP shape, or `undefined` when it must not be
400
+ * exported (unsampled, dropped, or an ignored failure).
401
+ */
402
+ const makeSpanEncoder = (filter) => {
403
+ const drop = filter.drop;
404
+ return (span, connectionLost) => {
405
+ if (!span.sampled) return void 0;
406
+ const status = span.status;
407
+ if (drop !== void 0 && drop({
408
+ name: span.name,
409
+ kind: span.kind,
410
+ attributes: span.attributes,
411
+ exit: status.exit
412
+ })) return void 0;
413
+ if (isIgnoredExit(status.exit, filter.isAnticipated)) return void 0;
414
+ return makeOtlpSpan(span, filter, connectionLost);
415
+ };
416
+ };
417
+ /**
418
+ * A Tracer whose finished spans go to `exportFn`. A span matching
419
+ * `dropSpanSubtrees` starts unsampled, which its descendants inherit (Effect
420
+ * only samples a child of a sampled parent), so the whole subtree is skipped.
421
+ * Only ever forced to `false`: trace levels and incoming `00` headers win.
422
+ */
423
+ const makeMapleTracer = (filter, exportFn, watchConnection) => {
424
+ const dropSubtree = filter.dropSubtree;
425
+ return Tracer.make({ span(spanOptions) {
426
+ const connectionLost = watchConnection?.();
427
+ return makeSpan({
428
+ ...spanOptions,
429
+ sampled: spanOptions.sampled && (dropSubtree === void 0 || !dropSubtree(spanOptions.name)),
430
+ status: {
431
+ _tag: "Started",
432
+ startTime: spanOptions.startTime
433
+ },
434
+ attributes: /* @__PURE__ */ new Map(),
435
+ export: (span) => exportFn(span, connectionLost)
436
+ });
437
+ } });
438
+ };
439
+ const makeSpanBuffer = (options = {}) => {
440
+ let buffer = [];
441
+ let disabled = false;
442
+ const filter = resolveSpanFilter(options);
443
+ const encode = makeSpanEncoder(filter);
444
+ const push = (span, connectionLost) => {
445
+ if (disabled) return;
446
+ if (buffer.length >= MAX_BUFFER) return;
447
+ const otlp = encode(span, connectionLost);
448
+ if (otlp !== void 0) buffer.push(otlp);
449
+ };
450
+ let unsettled = [];
451
+ const settle = () => {
452
+ const items = unsettled;
453
+ unsettled = [];
454
+ for (const [span, connectionLost] of items) push(span, connectionLost);
455
+ };
456
+ const exportFn = (span, connectionLost) => {
457
+ if (disabled) return;
458
+ if (connectionLost !== void 0 && connectionLost() === void 0 && isTransportFailure(span)) {
459
+ if (buffer.length + unsettled.length >= MAX_BUFFER) return;
460
+ unsettled.push([span, connectionLost]);
461
+ if (unsettled.length === 1) setTimeout(settle, 0);
462
+ return;
463
+ }
464
+ push(span, connectionLost);
465
+ };
466
+ const tracer = makeMapleTracer(filter, exportFn, options.watchConnection);
467
+ const captureException = (error, captureOptions = {}) => {
468
+ if (disabled) return;
469
+ const now = BigInt(Date.now()) * 1000000n;
470
+ makeSpan({
471
+ name: captureOptions.name ?? "exception",
472
+ parent: Option.none(),
473
+ annotations: Context.empty(),
474
+ status: {
475
+ _tag: "Started",
476
+ startTime: now
477
+ },
478
+ attributes: new Map(Object.entries(captureOptions.attributes ?? {})),
479
+ links: [],
480
+ sampled: true,
481
+ kind: "internal",
482
+ export: exportFn
483
+ }).end(now, Exit.failCause(Cause.die(error)));
484
+ };
485
+ return {
486
+ tracerLayer: Layer.succeed(Tracer.Tracer, tracer),
487
+ captureException,
488
+ drain: () => {
489
+ settle();
490
+ const items = buffer;
491
+ buffer = [];
492
+ return items;
493
+ },
494
+ restore: (items) => {
495
+ if (disabled || items.length === 0) return;
496
+ buffer = [...items, ...buffer].slice(0, MAX_BUFFER);
497
+ },
498
+ setDisabled: (value) => {
499
+ disabled = value;
500
+ if (value) {
501
+ buffer = [];
502
+ unsettled = [];
503
+ }
504
+ },
505
+ size: () => buffer.length + unsettled.length
506
+ };
507
+ };
508
+ const ATTR_EXCEPTION_TYPE = "exception.type";
509
+ const ATTR_EXCEPTION_MESSAGE = "exception.message";
510
+ const ATTR_EXCEPTION_STACKTRACE = "exception.stacktrace";
511
+ const SpanProto = {
512
+ _tag: "Span",
513
+ end(endTime, exit) {
514
+ this.status = {
515
+ _tag: "Ended",
516
+ startTime: this.status.startTime,
517
+ endTime,
518
+ exit
519
+ };
520
+ this.export(this);
521
+ },
522
+ attribute(key, value) {
523
+ this.attributes.set(key, value);
524
+ },
525
+ event(name, startTime, attributes) {
526
+ this.events.push([
527
+ name,
528
+ startTime,
529
+ attributes
530
+ ]);
531
+ },
532
+ addLinks(links) {
533
+ this.links.push(...links);
534
+ }
535
+ };
536
+ const makeSpan = (options) => {
537
+ const self = Object.assign(Object.create(SpanProto), options);
538
+ self.traceId = self.parent._tag === "Some" ? self.parent.value.traceId : generateId(32);
539
+ self.spanId = generateId(16);
540
+ self.events = [];
541
+ return self;
542
+ };
543
+ const generateId = (len) => {
544
+ const chars = "0123456789abcdef";
545
+ let result = "";
546
+ for (let i = 0; i < len; i++) result += chars[Math.floor(Math.random() * 16)];
547
+ return result;
548
+ };
549
+ const isFullyAnticipated = (cause, isAnticipated) => {
550
+ if (isAnticipated === void 0) return false;
551
+ if (cause.reasons.some(Cause.isDieReason)) return false;
552
+ const failErrors = cause.reasons.filter(Cause.isFailReason).map((reason) => reason.error);
553
+ return failErrors.length > 0 && failErrors.every(isAnticipated);
554
+ };
555
+ const renderedServerError = (self) => {
556
+ if (self.kind !== "server") return void 0;
557
+ const raw = self.attributes.get("http.response.status_code");
558
+ const code = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : NaN;
559
+ return Number.isInteger(code) && code >= 500 ? code : void 0;
560
+ };
561
+ const isStatusOnlyResponseError = (error) => Predicate.isTagged(error, "ResponseError") && (!Predicate.hasProperty(error, "cause") || error.cause === void 0) && (!Predicate.hasProperty(error, "description") || error.description === void 0);
562
+ const isResponseFailure = (exit) => {
563
+ if (exit._tag === "Success") return true;
564
+ const [reason, ...rest] = exit.cause.reasons;
565
+ return rest.length === 0 && reason !== void 0 && Cause.isFailReason(reason) && Predicate.isTagged(reason.error, "HttpServerError") && Predicate.hasProperty(reason.error, "reason") && isStatusOnlyResponseError(reason.error.reason);
566
+ };
567
+ const httpClientFailureReason = (exit, reasonTag) => {
568
+ if (exit._tag === "Success") return void 0;
569
+ const [reason, ...rest] = exit.cause.reasons;
570
+ if (rest.length > 0 || reason === void 0 || !Cause.isFailReason(reason)) return void 0;
571
+ const error = reason.error;
572
+ if (!Predicate.isTagged(error, "HttpClientError") || !Predicate.hasProperty(error, "reason")) return void 0;
573
+ return Predicate.isTagged(error.reason, reasonTag) ? error.reason : void 0;
574
+ };
575
+ const isTransportFailure = (span) => span.status._tag === "Ended" && httpClientFailureReason(span.status.exit, "TransportError") !== void 0;
576
+ const isClientStatus4xx = (self, exit) => {
577
+ if (self.kind !== "client") return false;
578
+ const reason = httpClientFailureReason(exit, "StatusCodeError");
579
+ if (!Predicate.hasProperty(reason, "response") || !Predicate.hasProperty(reason.response, "status")) return false;
580
+ const code = reason.response.status;
581
+ return typeof code === "number" && code >= 400 && code < 500;
582
+ };
583
+ const errorTypeOf = (self, exit) => {
584
+ const code = self.attributes.get("http.response.status_code");
585
+ if (typeof code === "number" && code >= 400) return String(code);
586
+ if (exit._tag === "Success") return "_OTHER";
587
+ const [reason] = exit.cause.reasons;
588
+ if (reason !== void 0 && Cause.isFailReason(reason)) {
589
+ const error = reason.error;
590
+ if (Predicate.isTagged(error, "HttpClientError") && Predicate.hasProperty(error, "reason")) {
591
+ const tag = Predicate.hasProperty(error.reason, "_tag") ? error.reason._tag : void 0;
592
+ if (typeof tag === "string") return tag;
593
+ }
594
+ }
595
+ return Cause.prettyErrors(exit.cause)[0]?.name || "_OTHER";
596
+ };
597
+ const makeOtlpSpan = (self, filter, connectionLost) => {
598
+ const status = self.status;
599
+ const attributes = OtlpResource.entriesToAttributes(self.attributes.entries());
600
+ const events = self.events.map(([name, startTime, attrs]) => ({
601
+ name,
602
+ timeUnixNano: String(startTime),
603
+ attributes: attrs ? OtlpResource.entriesToAttributes(Object.entries(attrs)) : [],
604
+ droppedAttributesCount: 0
605
+ }));
606
+ let otelStatus;
607
+ const lostReason = connectionLost !== void 0 && httpClientFailureReason(status.exit, "TransportError") !== void 0 ? connectionLost() : void 0;
608
+ const serverError = isResponseFailure(status.exit) ? renderedServerError(self) : void 0;
609
+ if (serverError !== void 0) {
610
+ const method = self.attributes.get("http.request.method");
611
+ const path = self.attributes.get("url.path");
612
+ const message = typeof method === "string" && typeof path === "string" ? `HTTP ${serverError} (${method} ${path})` : `HTTP ${serverError}`;
613
+ otelStatus = {
614
+ code: StatusCode.Error,
615
+ message
616
+ };
617
+ if (!events.some((event) => event.name === "exception")) events.push({
618
+ name: "exception",
619
+ timeUnixNano: String(status.endTime),
620
+ droppedAttributesCount: 0,
621
+ attributes: [{
622
+ key: ATTR_EXCEPTION_TYPE,
623
+ value: { stringValue: "HttpServerErrorResponse" }
624
+ }, {
625
+ key: ATTR_EXCEPTION_MESSAGE,
626
+ value: { stringValue: message }
627
+ }]
628
+ });
629
+ } else if (status.exit._tag === "Success") otelStatus = constOtelStatusSuccess;
630
+ else if (Cause.hasInterruptsOnly(status.exit.cause)) {
631
+ otelStatus = constOtelStatusSuccess;
632
+ attributes.push({
633
+ key: "mxl.interrupted",
634
+ value: { boolValue: true }
635
+ });
636
+ } else if (renderedServerError(self) === void 0 && isFullyAnticipated(status.exit.cause, filter.isAnticipated)) otelStatus = constOtelStatusSuccess;
637
+ else if (isClientStatus4xx(self, status.exit)) otelStatus = {
638
+ code: StatusCode.Error,
639
+ message: Cause.prettyErrors(status.exit.cause)[0]?.message ?? ""
640
+ };
641
+ else if (lostReason !== void 0) {
642
+ otelStatus = { code: StatusCode.Unset };
643
+ attributes.push({
644
+ key: "mxl.connection_lost",
645
+ value: { stringValue: lostReason }
646
+ });
647
+ } else {
648
+ const errors = Cause.prettyErrors(status.exit.cause, { includeCauseInStack: filter.includeCauseInStack });
649
+ otelStatus = { code: StatusCode.Error };
650
+ const firstError = errors[0];
651
+ if (firstError) {
652
+ otelStatus.message = firstError.message;
653
+ for (const error of errors) events.push({
654
+ name: "exception",
655
+ timeUnixNano: String(status.endTime),
656
+ droppedAttributesCount: 0,
657
+ attributes: [
658
+ {
659
+ key: ATTR_EXCEPTION_TYPE,
660
+ value: { stringValue: error.name }
661
+ },
662
+ {
663
+ key: ATTR_EXCEPTION_MESSAGE,
664
+ value: { stringValue: error.message }
665
+ },
666
+ {
667
+ key: ATTR_EXCEPTION_STACKTRACE,
668
+ value: { stringValue: error.stack ?? "No stack trace available" }
669
+ }
670
+ ]
671
+ });
672
+ }
673
+ }
674
+ if (otelStatus.code === StatusCode.Error && !self.attributes.has("error.type")) attributes.push({
675
+ key: "error.type",
676
+ value: { stringValue: errorTypeOf(self, status.exit) }
677
+ });
678
+ return {
679
+ traceId: self.traceId,
680
+ spanId: self.spanId,
681
+ parentSpanId: self.parent._tag === "Some" ? self.parent.value.spanId : void 0,
682
+ name: self.name,
683
+ kind: SpanKind[self.kind],
684
+ startTimeUnixNano: String(status.startTime),
685
+ endTimeUnixNano: String(status.endTime),
686
+ attributes,
687
+ droppedAttributesCount: 0,
688
+ events,
689
+ droppedEventsCount: 0,
690
+ status: otelStatus,
691
+ links: self.links.map((link) => ({
692
+ traceId: link.span.traceId,
693
+ spanId: link.span.spanId,
694
+ attributes: OtlpResource.entriesToAttributes(Object.entries(link.attributes)),
695
+ droppedAttributesCount: 0
696
+ })),
697
+ droppedLinksCount: 0
698
+ };
699
+ };
700
+ const StatusCode = {
701
+ Unset: 0,
702
+ Ok: 1,
703
+ Error: 2
704
+ };
705
+ const SpanKind = {
706
+ unspecified: 0,
707
+ internal: 1,
708
+ server: 2,
709
+ client: 3,
710
+ producer: 4,
711
+ consumer: 5
712
+ };
713
+ const constOtelStatusSuccess = { code: StatusCode.Ok };
714
+ // src/browser/replay.ts
715
+ const replayRecordOptions = (replay) => ({
716
+ maskAllInputs: replay?.maskAllInputs ?? true,
717
+ maskTextSelector: replay?.maskAllText === true ? "*" : void 0,
718
+ ...replay?.canvasFps && replay.canvasFps > 0 && replay.maskAllText !== true ? {
719
+ recordCanvas: true,
720
+ sampling: { canvas: replay.canvasFps }
721
+ } : {}
722
+ });
723
+ let stopRecorder;
724
+ let chunkSeq = 0;
725
+ const ingestHeaders = (clientId, apiKey, contentType) => {
726
+ const headers = {
727
+ "content-type": contentType,
728
+ "mxl-client-id": clientId,
729
+ "mxl-sdk-name": SDK_NAME,
730
+ "mxl-sdk-version": SDK_VERSION
731
+ };
732
+ if (apiKey) headers["mxl-api-key"] = apiKey;
733
+ return headers;
734
+ };
735
+ const isWebSdkPresent = () => {
736
+ const global = globalThis;
737
+ return Boolean(global.__MXL_WEB_SDK__ || global.MxlWebSdk || global.__MXL_REPLAY_SESSION__);
738
+ };
739
+ const shouldRecordReplay = (replay) => {
740
+ if (isWebSdkPresent()) return false;
741
+ if (replay?.enabled === false) return false;
742
+ const sampleRate = replay?.sampleRate ?? 1;
743
+ return sampleRate >= 1 || Math.random() < sampleRate;
744
+ };
745
+ const sessionMetaRow = (serviceName) => {
746
+ const identity = getCurrentIdentity();
747
+ const started = /* @__PURE__ */ new Date();
748
+ return {
749
+ service_name: serviceName,
750
+ session_id: getCurrentSessionId(),
751
+ start_time: started.toISOString().replace("T", " ").replace("Z", ""),
752
+ status: "active",
753
+ url_initial: typeof globalThis.location?.href === "string" ? globalThis.location.href : "",
754
+ user_agent: typeof globalThis.navigator?.userAgent === "string" ? globalThis.navigator.userAgent : "",
755
+ user_id: identity?.id ?? "",
756
+ version: 1
757
+ };
758
+ };
759
+ const postSessionReplayMeta = (ingestUrl, clientId, serviceName, apiKey) => {
760
+ try {
761
+ fetch(`${ingestUrl.replace(/\/$/, "")}/sessionReplays/meta`, {
762
+ body: `${JSON.stringify(sessionMetaRow(serviceName))}\n`,
763
+ headers: ingestHeaders(clientId, apiKey, "application/x-ndjson"),
764
+ keepalive: true,
765
+ method: "POST"
766
+ });
767
+ } catch {}
768
+ };
769
+ const postSessionReplayBlob = (ingestUrl, clientId, meta, body, apiKey) => {
770
+ try {
771
+ fetch(`${ingestUrl.replace(/\/$/, "")}/sessionReplays/blob`, {
772
+ body,
773
+ headers: {
774
+ ...ingestHeaders(clientId, apiKey, "application/octet-stream"),
775
+ "x-mxl-chunk-seq": String(meta.chunkSeq),
776
+ "x-mxl-duration-ms": String(meta.durationMs),
777
+ "x-mxl-event-count": String(meta.eventCount),
778
+ "x-mxl-is-checkpoint": meta.isCheckpoint ? "1" : "0",
779
+ "x-mxl-session-id": meta.sessionId
780
+ },
781
+ keepalive: true,
782
+ method: "POST"
783
+ });
784
+ } catch {}
785
+ };
786
+ const gzipJson = async (value) => {
787
+ if (typeof CompressionStream === "undefined") return;
788
+ const encoded = new TextEncoder().encode(JSON.stringify(value));
789
+ const stream = new Blob([encoded]).stream().pipeThrough(new CompressionStream("gzip"));
790
+ return new Uint8Array(await new Response(stream).arrayBuffer());
791
+ };
792
+ const stopReplay = () => {
793
+ stopRecorder?.();
794
+ stopRecorder = void 0;
795
+ };
796
+ const startReplay = async (config) => {
797
+ if (!shouldRecordReplay(config.replay)) return;
798
+ stopReplay();
799
+ chunkSeq = 0;
800
+ postSessionReplayMeta(config.ingestUrl, config.clientId, config.serviceName, config.apiKey);
801
+ try {
802
+ const rrweb = await import(
803
+ /* @vite-ignore */
804
+ "rrweb"
805
+ );
806
+ if (typeof rrweb.record !== "function") return;
807
+ const events = [];
808
+ const startedAt = Date.now();
809
+ const stop = rrweb.record({
810
+ emit: (event) => {
811
+ events.push(event);
812
+ },
813
+ ...replayRecordOptions(config.replay)
814
+ });
815
+ stopRecorder = () => {
816
+ stop?.();
817
+ if (events.length === 0) return;
818
+ gzipJson(events).then((gzipped) => {
819
+ if (!gzipped) return;
820
+ postSessionReplayBlob(config.ingestUrl, config.clientId, {
821
+ chunkSeq: chunkSeq++,
822
+ durationMs: Date.now() - startedAt,
823
+ eventCount: events.length,
824
+ isCheckpoint: true,
825
+ sessionId: getCurrentSessionId()
826
+ }, gzipped, config.apiKey);
827
+ });
828
+ };
829
+ } catch {}
830
+ };
831
+ // src/browser/layer.ts
832
+ var layer_exports = /* @__PURE__ */ __exportAll({
833
+ clearIdentity: () => clearIdentity,
834
+ identify: () => identify,
835
+ layer: () => layer,
836
+ stampUserIdOnSpan: () => stampUserIdOnSpan,
837
+ track: () => track
838
+ });
839
+ const stampUserIdOnSpan = (span) => {
840
+ const userId = getCurrentUserId();
841
+ if (userId) span.setAttribute("user.id", userId);
842
+ };
843
+ var UserIdSpanProcessor = class {
844
+ inner;
845
+ constructor(inner) {
846
+ this.inner = inner;
847
+ }
848
+ forceFlush() {
849
+ return this.inner.forceFlush();
850
+ }
851
+ onEnd(span) {
852
+ this.inner.onEnd(span);
853
+ }
854
+ onStart(span, parentContext) {
855
+ stampUserIdOnSpan(span);
856
+ this.inner.onStart(span, parentContext);
857
+ }
858
+ shutdown() {
859
+ return this.inner.shutdown();
860
+ }
861
+ };
862
+ function resolveBrowserEndpoint(config) {
863
+ if (config.disabled) return;
864
+ if (config.endpoint && config.endpoint.length > 0) return normalizeOtlpBaseUrl(config.endpoint);
865
+ }
866
+ /**
867
+ * Browser Effect observability layer backed by OpenTelemetry WebSdk.
868
+ *
869
+ * Returns a no-op layer when `disabled` is true or no OTLP endpoint is configured.
870
+ */
871
+ const layer = (config = {}) => {
872
+ if (config.clientId) {
873
+ configureBrowserAnalytics({
874
+ apiKey: config.apiKey,
875
+ clientId: config.clientId,
876
+ ingestUrl: config.ingestUrl,
877
+ privacy: config.privacy,
878
+ serviceName: config.serviceName
879
+ });
880
+ startReplay({
881
+ apiKey: config.apiKey,
882
+ clientId: config.clientId,
883
+ ingestUrl: config.ingestUrl ?? "http://127.0.0.1:8000",
884
+ replay: config.replay,
885
+ serviceName: config.serviceName ?? "unknown"
886
+ });
887
+ }
888
+ const endpoint = resolveBrowserEndpoint(config);
889
+ if (!endpoint) return Layer.empty;
890
+ const attributes = {
891
+ ...browserResourceAttributes(),
892
+ "mxl.sdk.type": "browser",
893
+ ...config.environment ? {
894
+ "deployment.environment": config.environment,
895
+ "deployment.environment.name": config.environment
896
+ } : {},
897
+ ...config.serviceNamespace ? { "service.namespace": config.serviceNamespace } : {},
898
+ ...config.attributes
899
+ };
900
+ return WebSdk.layer(() => ({
901
+ resource: {
902
+ attributes,
903
+ serviceName: config.serviceName ?? "unknown",
904
+ serviceVersion: config.serviceVersion
905
+ },
906
+ spanProcessor: new UserIdSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter({
907
+ headers: config.headers,
908
+ url: `${endpoint}/v1/traces`
909
+ })))
910
+ }));
911
+ };
912
+ const identify = (input) => Effect.sync(() => identify$1(input));
913
+ const clearIdentity = Effect.sync(() => clearIdentity$1());
914
+ const track = (name, props) => Effect.sync(() => track$1(name, props));
915
+ // src/browser/flushable.ts
916
+ var flushable_exports = /* @__PURE__ */ __exportAll({
917
+ attachProcessor: () => attachProcessor,
918
+ make: () => make
919
+ });
920
+ const processors = /* @__PURE__ */ new WeakMap();
921
+ const DEFAULT_AUTO_FLUSH_MS = 5e3;
922
+ const attachProcessor = (telemetry, processor) => {
923
+ processors.set(telemetry, processor);
924
+ };
925
+ const otlpAnyValue = (value) => {
926
+ if (Array.isArray(value)) return { arrayValue: { values: value.map(otlpAnyValue) } };
927
+ switch (typeof value) {
928
+ case "string": return { stringValue: value };
929
+ case "boolean": return { boolValue: value };
930
+ case "number": return Number.isInteger(value) ? { intValue: value } : { doubleValue: value };
931
+ case "bigint": return { intValue: Number(value) };
932
+ default: return { stringValue: String(value) };
933
+ }
934
+ };
935
+ const makeTracesBody = (input) => {
936
+ const attributes = Object.entries(input.attributes).map(([key, value]) => ({
937
+ key,
938
+ value: otlpAnyValue(value)
939
+ }));
940
+ attributes.push({
941
+ key: "service.name",
942
+ value: { stringValue: input.serviceName }
943
+ });
944
+ if (input.serviceVersion) attributes.push({
945
+ key: "service.version",
946
+ value: { stringValue: input.serviceVersion }
947
+ });
948
+ return { resourceSpans: [{
949
+ resource: {
950
+ attributes,
951
+ droppedAttributesCount: 0
952
+ },
953
+ scopeSpans: [{
954
+ scope: { name: input.serviceName },
955
+ spans: input.spans
956
+ }]
957
+ }] };
958
+ };
959
+ const resolveFlushableEndpoint = (config) => {
960
+ if (config.disabled) return void 0;
961
+ if (config.endpoint && config.endpoint.length > 0) return normalizeOtlpBaseUrl(config.endpoint);
962
+ };
963
+ const make = (config = {}) => {
964
+ const endpoint = resolveFlushableEndpoint(config);
965
+ const spanBuffer = endpoint ? makeSpanBuffer(config) : void 0;
966
+ const layer$1 = spanBuffer ? spanBuffer.tracerLayer : layer(config);
967
+ if (spanBuffer && config.clientId) layer({
968
+ ...config,
969
+ endpoint: void 0
970
+ });
971
+ const resourceAttributes = {
972
+ ...browserResourceAttributes(),
973
+ "mxl.sdk.type": "browser",
974
+ ...config.environment ? {
975
+ "deployment.environment": config.environment,
976
+ "deployment.environment.name": config.environment
977
+ } : {},
978
+ ...config.serviceNamespace ? { "service.namespace": config.serviceNamespace } : {},
979
+ ...config.attributes
980
+ };
981
+ const state = { disabledUntil: 0 };
982
+ const listeners = [];
983
+ const telemetry = {
984
+ captureException: (error, options) => {
985
+ if (spanBuffer) spanBuffer.captureException(error, options);
986
+ if (config.clientId) track$1(options?.name ?? "uncaught_error", {
987
+ ...options?.attributes,
988
+ message: error instanceof Error ? error.message : String(error)
989
+ });
990
+ },
991
+ dispose: async () => {
992
+ for (const stop of listeners) stop();
993
+ listeners.length = 0;
994
+ stopReplay();
995
+ await telemetry.flush();
996
+ },
997
+ flush: makeSerializedFlush(guardFlush("[MxlFlush]", async () => {
998
+ flushAnalytics(true);
999
+ await processors.get(telemetry)?.forceFlush();
1000
+ if (spanBuffer && endpoint) {
1001
+ const spans = spanBuffer.drain();
1002
+ if (spans.length > 0) await fetchTransport({
1003
+ body: JSON.stringify(makeTracesBody({
1004
+ attributes: resourceAttributes,
1005
+ serviceName: config.serviceName ?? "unknown",
1006
+ serviceVersion: config.serviceVersion,
1007
+ spans
1008
+ })),
1009
+ headers: {
1010
+ "content-type": "application/json",
1011
+ ...config.headers
1012
+ },
1013
+ keepalive: true,
1014
+ url: `${endpoint}/v1/traces`
1015
+ });
1016
+ }
1017
+ }, state)),
1018
+ layer: layer$1
1019
+ };
1020
+ const intervalMs = config.autoFlushInterval === void 0 ? DEFAULT_AUTO_FLUSH_MS : config.autoFlushInterval === false ? 0 : config.autoFlushInterval;
1021
+ if (intervalMs > 0) {
1022
+ const timer = setInterval(() => {
1023
+ telemetry.flush();
1024
+ }, intervalMs);
1025
+ timer.unref?.();
1026
+ listeners.push(() => {
1027
+ clearInterval(timer);
1028
+ });
1029
+ }
1030
+ const documentRef = globalThis.document;
1031
+ if (config.flushOnUnload !== false) {
1032
+ const onHidden = () => {
1033
+ telemetry.flush();
1034
+ };
1035
+ if (documentRef) {
1036
+ const onVisibilityChange = () => {
1037
+ if (documentRef.visibilityState === "hidden") onHidden();
1038
+ };
1039
+ documentRef.addEventListener("visibilitychange", onVisibilityChange);
1040
+ listeners.push(() => {
1041
+ documentRef.removeEventListener("visibilitychange", onVisibilityChange);
1042
+ });
1043
+ }
1044
+ if (typeof globalThis.addEventListener === "function") {
1045
+ globalThis.addEventListener("pagehide", onHidden);
1046
+ listeners.push(() => {
1047
+ globalThis.removeEventListener("pagehide", onHidden);
1048
+ });
1049
+ }
1050
+ }
1051
+ if (config.captureGlobalErrors !== false && typeof globalThis.addEventListener === "function") {
1052
+ const onError = (event) => {
1053
+ const record = event;
1054
+ telemetry.captureException(record.error ?? record.reason ?? event);
1055
+ telemetry.flush();
1056
+ };
1057
+ globalThis.addEventListener("error", onError);
1058
+ globalThis.addEventListener("unhandledrejection", onError);
1059
+ listeners.push(() => {
1060
+ globalThis.removeEventListener("error", onError);
1061
+ globalThis.removeEventListener("unhandledrejection", onError);
1062
+ });
1063
+ }
1064
+ return telemetry;
1065
+ };
1066
+ export { scrubUrl as _, browserDocument as a, clearIdentity$1 as c, increment as d, setConsent as f, resetUrlSanitizersForTests as g, redactUrl as h, replayRecordOptions as i, decrement as l, addUrlSanitizer as m, make as n, browserNavigator as o, track$1 as p, layer_exports as r, browserResourceAttributes as s, flushable_exports as t, identify$1 as u };
1067
+
1068
+ //# sourceMappingURL=flushable-CXe-q-GF.js.map