@safaricom-mxl/effect-sdk 0.0.15 → 0.0.17

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,587 @@
1
+ import { Cause, Config, Context, Duration, Effect, Layer, ManagedRuntime, Match, Option, Schedule, Schema, Semaphore } from "effect";
2
+ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
3
+ import { SchemaJITCompiler } from "effect/unstable/schema";
4
+ import { Otlp } from "effect/unstable/observability";
5
+ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
6
+ import * as Otel from "@opentelemetry/api";
7
+ import * as Tracer from "effect/Tracer";
8
+ import { v7 } from "uuid";
9
+ import { platform, provider, runtime } from "std-env";
10
+ const RESERVED_EVENT_NAMES = ["session_start", "session_end"];
11
+ const isReservedEventName = (name) => RESERVED_EVENT_NAMES.some((reserved) => reserved === name);
12
+ const coerceTrackProps = (props) => {
13
+ if (props === void 0) return {};
14
+ const attributes = {};
15
+ for (const [key, value] of Object.entries(props)) {
16
+ if (value === void 0) continue;
17
+ attributes[key] = typeof value === "string" ? value : String(value);
18
+ }
19
+ return attributes;
20
+ };
21
+ const toEventTimestamp = (value) => (value instanceof Date ? value : new Date(value ?? Date.now())).toISOString();
22
+ const encodeProductEventLine = (input) => {
23
+ const trimmed = typeof input.name === "string" ? input.name.trim() : "";
24
+ if (trimmed.length === 0 || isReservedEventName(trimmed)) return;
25
+ const options = input.options ?? {};
26
+ return {
27
+ attributes: coerceTrackProps(options.attributes),
28
+ group_id: options.groupId ?? "",
29
+ name: trimmed.slice(0, 80),
30
+ page_path: options.pagePath ?? "",
31
+ service_name: input.serviceName,
32
+ session_id: options.sessionId ?? "",
33
+ source: input.source,
34
+ timestamp: toEventTimestamp(options.timestamp),
35
+ traceparent: options.traceparent,
36
+ url: options.url ?? "",
37
+ user_id: options.userId ?? "",
38
+ visitor_id: options.visitorId ?? ""
39
+ };
40
+ };
41
+ const encodeNdjson = (lines) => `${lines.map((line) => JSON.stringify(line)).join("\n")}\n`;
42
+ //#endregion
43
+ //#region src/version.ts
44
+ const SDK_NAME = "mxl-effect-sdk";
45
+ const SDK_VERSION = "0.0.15";
46
+ //#endregion
47
+ //#region src/server/events.ts
48
+ const DEFAULT_FLUSH_INTERVAL = Duration.seconds(5);
49
+ const DEFAULT_MAX_BATCH_SIZE = 100;
50
+ const MAX_BUFFERED_EVENTS = 1e3;
51
+ const WARN_INTERVAL_MS = 3e4;
52
+ const DEFAULT_BASKET_URL = "http://127.0.0.1:8000";
53
+ var MxlEvents = class extends Context.Service()("@safaricom-mxl/effect-sdk/MxlEvents") {};
54
+ var ProductEventsPostError = class extends Schema.TaggedError()("@safaricom-mxl/effect-sdk/ProductEventsPostError", {
55
+ cause: Schema.optionalKey(Schema.Defect()),
56
+ message: Schema.String,
57
+ status: Schema.optionalKey(Schema.Number)
58
+ }) {};
59
+ SchemaJITCompiler.enable(ProductEventsPostError.ast);
60
+ const stringOrUndefined$1 = (value) => value && value.length > 0 ? value : void 0;
61
+ const resolveEventsConfig$1 = (config) => {
62
+ const endpoint = (config.endpoint ?? process.env.MXL_BASKET_URL ?? DEFAULT_BASKET_URL).replace(/\/$/, "");
63
+ return {
64
+ apiKey: stringOrUndefined$1(config.apiKey ?? process.env.MXL_API_KEY),
65
+ clientId: stringOrUndefined$1(config.clientId ?? process.env.MXL_CLIENT_ID),
66
+ serviceName: config.serviceName ?? process.env.OTEL_SERVICE_NAME ?? "unknown",
67
+ url: `${endpoint}${config.eventsPath ?? "/events"}`
68
+ };
69
+ };
70
+ const make$1 = Effect.fn("MxlEvents.make")(function* (config = {}) {
71
+ const resolved = resolveEventsConfig$1(config);
72
+ const maxBatchSize = Math.max(1, config.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE);
73
+ const client = (yield* HttpClient.HttpClient).pipe(HttpClient.filterStatusOk);
74
+ const flushing = yield* Semaphore.make(1);
75
+ let buffer = [];
76
+ let warnedAboutName = false;
77
+ let warnedAboutClient = false;
78
+ let lastPostWarnAt = 0;
79
+ const warnPost = (error) => {
80
+ const now = Date.now();
81
+ if (now - lastPostWarnAt < WARN_INTERVAL_MS) return;
82
+ lastPostWarnAt = now;
83
+ console.warn(`[MxlEvents] ${error.message} (dropping batch)`, error.cause ?? "");
84
+ };
85
+ const warnMissingClient = () => {
86
+ if (warnedAboutClient) return;
87
+ warnedAboutClient = true;
88
+ console.warn("[MxlEvents] no client id — set MXL_CLIENT_ID or pass `clientId`; dropping events");
89
+ };
90
+ const warnEmptyName = () => {
91
+ if (warnedAboutName) return;
92
+ warnedAboutName = true;
93
+ console.warn("[MxlEvents] track() needs a non-empty event name; the call was ignored.");
94
+ };
95
+ const post = (lines) => {
96
+ const headers = {
97
+ "mxl-client-id": resolved.clientId ?? "",
98
+ "mxl-sdk-name": SDK_NAME,
99
+ "mxl-sdk-version": SDK_VERSION,
100
+ "user-agent": `${SDK_NAME}-server/${SDK_VERSION}`
101
+ };
102
+ if (resolved.apiKey) headers["mxl-api-key"] = resolved.apiKey;
103
+ return HttpClientRequest.post(resolved.url).pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(encodeNdjson(lines), "application/x-ndjson"), client.execute, Effect.asVoid, Effect.mapError((error) => error.response !== void 0 ? new ProductEventsPostError({
104
+ message: `POST ${resolved.url} → ${error.response.status}`,
105
+ status: error.response.status
106
+ }) : new ProductEventsPostError({
107
+ cause: error,
108
+ message: `POST ${resolved.url} failed: ${error.message}`
109
+ })));
110
+ };
111
+ const flush = flushing.withPermits(1)(Effect.suspend(() => {
112
+ if (buffer.length === 0) return Effect.void;
113
+ const batch = buffer;
114
+ buffer = [];
115
+ if (resolved.clientId === void 0) {
116
+ warnMissingClient();
117
+ return Effect.void;
118
+ }
119
+ return post(batch).pipe(Effect.catchCause((cause) => Effect.sync(() => {
120
+ warnPost(Option.getOrElse(Cause.findErrorOption(cause), () => new ProductEventsPostError({
121
+ cause: Cause.squash(cause),
122
+ message: `POST ${resolved.url} failed`
123
+ })));
124
+ })));
125
+ }));
126
+ const track = (name, options = {}) => Effect.sync(() => {
127
+ const line = encodeProductEventLine({
128
+ name,
129
+ options,
130
+ serviceName: resolved.serviceName,
131
+ source: "server"
132
+ });
133
+ if (line === void 0) {
134
+ warnEmptyName();
135
+ return false;
136
+ }
137
+ buffer.push(line);
138
+ if (buffer.length > MAX_BUFFERED_EVENTS) buffer.splice(0, buffer.length - MAX_BUFFERED_EVENTS);
139
+ return buffer.length >= maxBatchSize;
140
+ }).pipe(Effect.flatMap((full) => full ? Effect.forkDetach(flush) : Effect.void), Effect.asVoid);
141
+ const interval = config.flushInterval ?? DEFAULT_FLUSH_INTERVAL;
142
+ if (Duration.toMillis(interval) > 0) yield* Effect.forkScoped(flush.pipe(Effect.schedule(Schedule.spaced(interval))));
143
+ yield* Effect.addFinalizer(() => flush);
144
+ return {
145
+ flush,
146
+ track
147
+ };
148
+ });
149
+ const layer$1 = (config = {}) => Layer.effect(MxlEvents, make$1(config)).pipe(Layer.provide(FetchHttpClient.layer));
150
+ const makeHandle = (config = {}) => {
151
+ const runtime = ManagedRuntime.make(layer$1(config));
152
+ const swallow = (promise) => promise.then(() => void 0, () => void 0);
153
+ return {
154
+ dispose: () => swallow(runtime.dispose()),
155
+ flush: () => swallow(runtime.runPromise(Effect.gen(function* () {
156
+ yield* (yield* MxlEvents).flush;
157
+ }))),
158
+ track: (name, options) => {
159
+ swallow(runtime.runPromise(Effect.gen(function* () {
160
+ yield* (yield* MxlEvents).track(name, options);
161
+ })));
162
+ }
163
+ };
164
+ };
165
+ //#endregion
166
+ //#region src/server/otel-context-bridge.ts
167
+ const evaluate = "~effect/Effect/evaluate";
168
+ let contextManagerInstalled = false;
169
+ /**
170
+ * Install AsyncLocalStorage so `Otel.context.with` actually propagates.
171
+ * Does not replace a context manager the host app already registered.
172
+ */
173
+ function ensureOtelContextManager() {
174
+ if (contextManagerInstalled) return;
175
+ const manager = new AsyncLocalStorageContextManager();
176
+ manager.enable();
177
+ if (!Otel.context.setGlobalContextManager(manager)) {
178
+ manager.disable();
179
+ contextManagerInstalled = true;
180
+ return;
181
+ }
182
+ contextManagerInstalled = true;
183
+ }
184
+ /**
185
+ * Put Effect span ids on the global OpenTelemetry context.
186
+ *
187
+ * Id-only `setSpanContext`. Live `Otel.Span` objects come from
188
+ * `OtelTracer.currentOtelSpan` in Effect space, not from a private TypeId.
189
+ */
190
+ function effectSpanToOtelContext(activeContext, span) {
191
+ return Otel.trace.setSpanContext(activeContext, {
192
+ isRemote: span._tag === "ExternalSpan",
193
+ spanId: span.spanId,
194
+ traceFlags: span.sampled ? Otel.TraceFlags.SAMPLED : Otel.TraceFlags.NONE,
195
+ traceId: span.traceId
196
+ });
197
+ }
198
+ /**
199
+ * Wrap a tracer so Effect spans activate global `@opentelemetry/api` context.
200
+ *
201
+ * Prefer this over `Otlp.layerJson({ tracerContext })`. That callback receives
202
+ * `(primitive, span)` and must return the evaluation, but `EffectPrimitive`
203
+ * only evaluates with a `fiber`. Keep this wrap until Effect passes fiber.
204
+ */
205
+ function wrapTracerWithOtelContextBridge(base) {
206
+ ensureOtelContextManager();
207
+ const existingContext = base.context;
208
+ return Tracer.make({
209
+ span(options) {
210
+ return base.span(options);
211
+ },
212
+ context(primitive, fiber) {
213
+ if (fiber.cache.span === void 0) {
214
+ if (existingContext) return existingContext(primitive, fiber);
215
+ return primitive[evaluate](fiber);
216
+ }
217
+ return Otel.context.with(effectSpanToOtelContext(Otel.context.active(), fiber.cache.span), () => {
218
+ if (existingContext) return existingContext(primitive, fiber);
219
+ return primitive[evaluate](fiber);
220
+ });
221
+ }
222
+ });
223
+ }
224
+ /**
225
+ * Layer that wraps the configured Effect tracer with a global OTel context bridge.
226
+ *
227
+ * Must be provided over `Otlp.layerJson` so queue propagation, log correlation,
228
+ * and outbound W3C headers see the active server span.
229
+ */
230
+ const otelGlobalContextBridgeLayer = Layer.effect(Tracer.Tracer, Effect.gen(function* () {
231
+ return wrapTracerWithOtelContextBridge(yield* Tracer.Tracer);
232
+ }));
233
+ //#endregion
234
+ //#region src/server/otlp.ts
235
+ function normalizeOtlpBaseUrl(url) {
236
+ const trimmed = url.replace(/\/$/, "");
237
+ if (trimmed.endsWith("/v1/traces") || trimmed.endsWith("/v1/logs")) return trimmed.replace(/\/v1\/(traces|logs|metrics)$/, "");
238
+ return trimmed;
239
+ }
240
+ /** Resolve the OTLP exporter endpoint from standard env vars. */
241
+ const endpoint = Effect.gen(function* () {
242
+ const otlp = yield* Config.option(Config.String("OTEL_EXPORTER_OTLP_ENDPOINT"));
243
+ if (Option.isSome(otlp)) return otlp;
244
+ return yield* Config.option(Config.String("OTLP_ENDPOINT"));
245
+ });
246
+ const sdkDisabled = Config.Boolean("OTEL_SDK_DISABLED").pipe(Config.withDefault(false));
247
+ /**
248
+ * Resolve service version / commit SHA from platform-specific env vars.
249
+ *
250
+ * Priority: npm_package_version > COMMIT_SHA > RAILWAY_GIT_COMMIT_SHA >
251
+ * VERCEL_GIT_COMMIT_SHA > CF_PAGES_COMMIT_SHA > RENDER_GIT_COMMIT
252
+ */
253
+ const serviceVersion = Config.option(Config.String("npm_package_version").pipe(Config.orElse(() => Config.String("COMMIT_SHA")), Config.orElse(() => Config.String("RAILWAY_GIT_COMMIT_SHA")), Config.orElse(() => Config.String("VERCEL_GIT_COMMIT_SHA")), Config.orElse(() => Config.String("CF_PAGES_COMMIT_SHA")), Config.orElse(() => Config.String("RENDER_GIT_COMMIT"))));
254
+ /**
255
+ * Resolve deployment environment from env vars.
256
+ *
257
+ * Priority: NODE_ENV > DEPLOYMENT_ENV > "development"
258
+ */
259
+ const environment = Config.String("NODE_ENV").pipe(Config.orElse(() => Config.String("DEPLOYMENT_ENV")), Config.withDefault("development"));
260
+ /** OTel-standard service name override. */
261
+ const otelServiceName = Config.option(Config.String("OTEL_SERVICE_NAME"));
262
+ /**
263
+ * Parse the OTel-standard `OTEL_RESOURCE_ATTRIBUTES` env var.
264
+ *
265
+ * Format per the OTel spec: comma-separated `key=value` pairs, values may be
266
+ * URL-encoded.
267
+ */
268
+ const parseOtelResourceAttributes = (input) => {
269
+ const result = {};
270
+ for (const pair of input.split(",")) {
271
+ const eq = pair.indexOf("=");
272
+ if (eq === -1) continue;
273
+ const key = pair.slice(0, eq).trim();
274
+ if (!key) continue;
275
+ const raw = pair.slice(eq + 1).trim();
276
+ let value = raw;
277
+ try {
278
+ value = decodeURIComponent(raw);
279
+ } catch {}
280
+ result[key] = value;
281
+ }
282
+ return result;
283
+ };
284
+ /** Resolve and parse `OTEL_RESOURCE_ATTRIBUTES`. */
285
+ const otelResourceAttributes = Effect.gen(function* () {
286
+ const raw = yield* Config.option(Config.String("OTEL_RESOURCE_ATTRIBUTES"));
287
+ return Option.match(raw, {
288
+ onNone: () => ({}),
289
+ onSome: (value) => parseOtelResourceAttributes(value)
290
+ });
291
+ });
292
+ /**
293
+ * Parse `OTEL_EXPORTER_OTLP_HEADERS` (`key=value,key2=value2`) into a header map.
294
+ */
295
+ const parseOtlpHeaders = (input) => {
296
+ const result = {};
297
+ for (const pair of input.split(",")) {
298
+ const eq = pair.indexOf("=");
299
+ if (eq === -1) continue;
300
+ const key = pair.slice(0, eq).trim();
301
+ if (!key) continue;
302
+ const raw = pair.slice(eq + 1).trim();
303
+ try {
304
+ result[key] = decodeURIComponent(raw);
305
+ } catch {
306
+ result[key] = raw;
307
+ }
308
+ }
309
+ return result;
310
+ };
311
+ /** Resolve OTLP export headers from env. */
312
+ const otlpHeaders = Effect.gen(function* () {
313
+ const raw = yield* Config.option(Config.String("OTEL_EXPORTER_OTLP_HEADERS"));
314
+ return Option.match(raw, {
315
+ onNone: () => ({}),
316
+ onSome: (value) => parseOtlpHeaders(value)
317
+ });
318
+ });
319
+ //#endregion
320
+ //#region src/server/platform.ts
321
+ const empty = {};
322
+ const runtimeAttrs = (rt) => Match.value(rt).pipe(Match.when("node", () => ({ "process.runtime.name": "nodejs" })), Match.whenOr("bun", "deno", "workerd", "edge-light", "fastly", (name) => ({ "process.runtime.name": name })), Match.orElse(() => empty));
323
+ const osAttrs = (plat) => Match.value(plat).pipe(Match.when("win32", () => ({ "os.type": "windows" })), Match.whenOr("darwin", "linux", "freebsd", "openbsd", "netbsd", (p) => ({ "os.type": p })), Match.orElse(() => empty));
324
+ const archAttrs = (a) => a === "" ? empty : { "host.arch": Match.value(a).pipe(Match.when("x64", () => "amd64"), Match.when("ia32", () => "x86"), Match.orElse((other) => other)) };
325
+ const lambdaAttrs = (env) => ({
326
+ "cloud.provider": "aws",
327
+ "cloud.platform": "aws_lambda",
328
+ "faas.name": env.AWS_LAMBDA_FUNCTION_NAME,
329
+ ...env.AWS_LAMBDA_FUNCTION_VERSION && { "faas.version": env.AWS_LAMBDA_FUNCTION_VERSION },
330
+ ...env.AWS_LAMBDA_LOG_STREAM_NAME && { "faas.instance": env.AWS_LAMBDA_LOG_STREAM_NAME },
331
+ ...env.AWS_REGION ? { "cloud.region": env.AWS_REGION } : env.AWS_DEFAULT_REGION ? { "cloud.region": env.AWS_DEFAULT_REGION } : {}
332
+ });
333
+ const providerAttrs = (prov, env) => Match.value(prov).pipe(Match.when("cloudflare_workers", () => ({
334
+ "cloud.provider": "cloudflare",
335
+ "cloud.platform": "cloudflare.workers"
336
+ })), Match.when("cloudflare_pages", () => ({
337
+ "cloud.provider": "cloudflare",
338
+ "cloud.platform": "cloudflare.pages"
339
+ })), Match.when("vercel", () => ({
340
+ "cloud.provider": "vercel",
341
+ "cloud.platform": "vercel",
342
+ ...env.VERCEL_REGION && { "cloud.region": env.VERCEL_REGION },
343
+ ...env.VERCEL_DEPLOYMENT_ID && { "faas.instance": env.VERCEL_DEPLOYMENT_ID }
344
+ })), Match.when("netlify", () => ({
345
+ "cloud.provider": "netlify",
346
+ "cloud.platform": "netlify"
347
+ })), Match.whenOr("google_cloudrun", "google_cloudrun_job", () => ({
348
+ "cloud.provider": "gcp",
349
+ "cloud.platform": "gcp_cloud_run",
350
+ ...env.K_SERVICE && { "faas.name": env.K_SERVICE },
351
+ ...env.K_REVISION && { "faas.version": env.K_REVISION },
352
+ ...env.CLOUD_RUN_REGION && { "cloud.region": env.CLOUD_RUN_REGION }
353
+ })), Match.when("firebase_app_hosting", () => ({
354
+ "cloud.provider": "gcp",
355
+ "cloud.platform": "gcp_firebase_app_hosting"
356
+ })), Match.when("aws_amplify", () => ({
357
+ "cloud.provider": "aws",
358
+ "cloud.platform": "aws_amplify"
359
+ })), Match.when("deno-deploy", () => ({
360
+ "cloud.provider": "deno",
361
+ "cloud.platform": "deno_deploy",
362
+ ...env.DENO_REGION && { "cloud.region": env.DENO_REGION },
363
+ ...env.DENO_DEPLOYMENT_ID && { "faas.instance": env.DENO_DEPLOYMENT_ID }
364
+ })), Match.when("render", () => ({
365
+ "cloud.provider": "render",
366
+ "cloud.platform": "render",
367
+ ...env.RENDER_INSTANCE_ID && { "faas.instance": env.RENDER_INSTANCE_ID }
368
+ })), Match.when("railway", () => ({
369
+ "cloud.provider": "railway",
370
+ "cloud.platform": "railway",
371
+ ...env.RAILWAY_REPLICA_ID && { "faas.instance": env.RAILWAY_REPLICA_ID }
372
+ })), Match.when("edgeone_pages", () => ({
373
+ "cloud.provider": "tencent_cloud",
374
+ "cloud.platform": "tencent_edgeone_pages"
375
+ })), Match.orElse(() => empty));
376
+ const workerdFallback = (rt, alreadyResolved) => rt === "workerd" && !alreadyResolved ? {
377
+ "cloud.provider": "cloudflare",
378
+ "cloud.platform": "cloudflare.workers"
379
+ } : empty;
380
+ const vercelEdgeFallback = (rt, alreadyResolved, env) => rt === "edge-light" && !alreadyResolved && env.VERCEL ? {
381
+ "cloud.provider": "vercel",
382
+ "cloud.platform": "vercel"
383
+ } : empty;
384
+ const derivePlatformAttributes = (inputs) => {
385
+ const { runtime: rt, provider: prov, platform: plat, arch, env } = inputs;
386
+ const cloudAttrs = env.AWS_LAMBDA_FUNCTION_NAME ? lambdaAttrs(env) : providerAttrs(prov, env);
387
+ const cloudResolved = "cloud.provider" in cloudAttrs;
388
+ return {
389
+ ...rt && { "mxl.runtime": rt },
390
+ ...prov && { "mxl.provider": prov },
391
+ ...runtimeAttrs(rt),
392
+ ...osAttrs(plat),
393
+ ...archAttrs(arch),
394
+ ...cloudAttrs,
395
+ ...workerdFallback(rt, cloudResolved),
396
+ ...vercelEdgeFallback(rt, cloudResolved, env)
397
+ };
398
+ };
399
+ const PLATFORM_ENV_KEYS = [
400
+ "AWS_LAMBDA_FUNCTION_NAME",
401
+ "AWS_LAMBDA_FUNCTION_VERSION",
402
+ "AWS_LAMBDA_LOG_STREAM_NAME",
403
+ "AWS_REGION",
404
+ "AWS_DEFAULT_REGION",
405
+ "VERCEL",
406
+ "VERCEL_REGION",
407
+ "VERCEL_DEPLOYMENT_ID",
408
+ "K_SERVICE",
409
+ "K_REVISION",
410
+ "CLOUD_RUN_REGION",
411
+ "DENO_REGION",
412
+ "DENO_DEPLOYMENT_ID",
413
+ "RENDER_INSTANCE_ID",
414
+ "RAILWAY_REPLICA_ID"
415
+ ];
416
+ const getAutoPlatformAttributes = () => {
417
+ const proc = globalThis.process;
418
+ const procEnv = proc?.env ?? {};
419
+ const env = {};
420
+ for (const key of PLATFORM_ENV_KEYS) env[key] = procEnv[key];
421
+ return derivePlatformAttributes({
422
+ runtime: runtime ?? "",
423
+ provider: provider ?? "",
424
+ platform: platform ?? "",
425
+ arch: proc?.arch ?? "",
426
+ env
427
+ });
428
+ };
429
+ //#endregion
430
+ //#region src/server/resource.ts
431
+ const stringOrUndefined = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
432
+ const DEFAULT_VCS_REPOSITORY_URL = "https://gitlab.safaricom.co.ke/mxl/apm/mxl-effect-sdk";
433
+ function resolveCommitRevision() {
434
+ return stringOrUndefined(process.env.COMMIT_SHA) ?? stringOrUndefined(process.env.VERCEL_GIT_COMMIT_SHA) ?? stringOrUndefined(process.env.GITHUB_SHA) ?? stringOrUndefined(process.env.RAILWAY_GIT_COMMIT_SHA) ?? stringOrUndefined(process.env.CF_PAGES_COMMIT_SHA) ?? stringOrUndefined(process.env.RENDER_GIT_COMMIT);
435
+ }
436
+ let serviceInstanceId;
437
+ const getServiceInstanceId = () => serviceInstanceId ??= v7();
438
+ const resolveResource = (config) => Effect.gen(function* () {
439
+ if (yield* sdkDisabled) return {
440
+ endpoint: void 0,
441
+ environment: void 0,
442
+ headers: void 0,
443
+ resource: {
444
+ attributes: {},
445
+ serviceName: config.serviceName ?? "unknown",
446
+ serviceVersion: void 0
447
+ }
448
+ };
449
+ const envEndpoint = yield* endpoint;
450
+ const endpoint$1 = config.endpoint ?? Option.getOrUndefined(envEndpoint) ?? "http://127.0.0.1:4318";
451
+ const envHeaders = yield* otlpHeaders;
452
+ const headers = config.headers && Object.keys(config.headers).length > 0 ? config.headers : Object.keys(envHeaders).length > 0 ? envHeaders : void 0;
453
+ const envServiceVersion = yield* serviceVersion;
454
+ const serviceVersion$1 = config.serviceVersion ?? Option.getOrUndefined(envServiceVersion);
455
+ const envEnvironment = yield* environment;
456
+ const environment$1 = config.environment ?? envEnvironment;
457
+ const envOtelServiceName = yield* otelServiceName;
458
+ const serviceName = config.serviceName ?? Option.getOrUndefined(envOtelServiceName) ?? "unknown";
459
+ const envResourceAttributes = yield* otelResourceAttributes;
460
+ const attributes = {};
461
+ Object.assign(attributes, getAutoPlatformAttributes());
462
+ attributes["mxl.sdk.type"] = config.sdkType ?? "server";
463
+ attributes["service.instance.id"] = getServiceInstanceId();
464
+ if (environment$1) {
465
+ attributes["deployment.environment"] = environment$1;
466
+ attributes["deployment.environment.name"] = environment$1;
467
+ }
468
+ if (serviceVersion$1) attributes["service.version"] = serviceVersion$1;
469
+ const commitRevision = resolveCommitRevision();
470
+ if (commitRevision) {
471
+ attributes["deployment.commit_sha"] = commitRevision;
472
+ attributes["vcs.ref.head.revision"] = commitRevision;
473
+ }
474
+ attributes["vcs.repository.url.full"] = stringOrUndefined(process.env.MXL_VCS_REPOSITORY_URL) ?? DEFAULT_VCS_REPOSITORY_URL;
475
+ Object.assign(attributes, envResourceAttributes);
476
+ if (config.attributes) Object.assign(attributes, config.attributes);
477
+ return {
478
+ endpoint: endpoint$1,
479
+ environment: environment$1,
480
+ headers,
481
+ resource: {
482
+ serviceName,
483
+ serviceVersion: serviceVersion$1,
484
+ attributes
485
+ }
486
+ };
487
+ }).pipe(Effect.orDie);
488
+ //#endregion
489
+ //#region src/server/layer.ts
490
+ /**
491
+ * Create an Effect Layer that provides OpenTelemetry traces, logs, and metrics
492
+ * configured for MXL's OTLP collector.
493
+ *
494
+ * Returns a no-op layer when `OTEL_SDK_DISABLED=true`.
495
+ *
496
+ * Wraps the OTLP tracer with `otelGlobalContextBridgeLayer` via `provideMerge`.
497
+ * `Otlp.layerJson({ tracerContext })` cannot evaluate without a `fiber`, and
498
+ * `Layer.updateService` cannot wrap a Tracer the OTLP layer already provides.
499
+ */
500
+ const layer = (config = {}) => Layer.unwrap(Effect.gen(function* () {
501
+ const resolved = yield* resolveResource({
502
+ ...config,
503
+ sdkType: "server"
504
+ });
505
+ if (!resolved.endpoint) return Layer.empty;
506
+ const otlp = Otlp.layerJson({
507
+ baseUrl: normalizeOtlpBaseUrl(resolved.endpoint),
508
+ headers: resolved.headers,
509
+ resource: resolved.resource,
510
+ maxBatchSize: config.maxBatchSize,
511
+ loggerExportInterval: config.loggerExportInterval,
512
+ metricsExportInterval: config.metricsExportInterval,
513
+ tracerExportInterval: config.tracerExportInterval,
514
+ shutdownTimeout: config.shutdownTimeout
515
+ }).pipe(Layer.provide(FetchHttpClient.layer));
516
+ return otelGlobalContextBridgeLayer.pipe(Layer.provideMerge(otlp));
517
+ }));
518
+ //#endregion
519
+ //#region src/shared/flush-core.ts
520
+ const COOLDOWN_MS = 6e4;
521
+ const makeSerializedFlush = (flush) => {
522
+ let pending = Promise.resolve();
523
+ return (...args) => {
524
+ pending = pending.then(() => flush(...args), () => flush(...args));
525
+ return pending;
526
+ };
527
+ };
528
+ const guardFlush = (prefix, flush, state) => {
529
+ return async (...args) => {
530
+ if (state && Date.now() < state.disabledUntil) return;
531
+ try {
532
+ await flush(...args);
533
+ } catch (error) {
534
+ if (state) state.disabledUntil = Date.now() + COOLDOWN_MS;
535
+ console.error(`${prefix} flush failed; cooling down 60s`, error);
536
+ }
537
+ };
538
+ };
539
+ //#endregion
540
+ //#region src/cloudflare/index.ts
541
+ var WorkerEnvironment = class extends Context.Service()("Cloudflare.Workers.WorkerEnvironment") {};
542
+ const stringFromEnv = (env, key) => {
543
+ const value = env[key];
544
+ return typeof value === "string" && value.length > 0 ? value : void 0;
545
+ };
546
+ const resolveEventsConfig = (env, config) => ({
547
+ apiKey: config.apiKey ?? stringFromEnv(env, "MXL_API_KEY"),
548
+ clientId: config.clientId ?? stringFromEnv(env, "MXL_CLIENT_ID"),
549
+ endpoint: config.ingestUrl ?? stringFromEnv(env, "MXL_BASKET_URL") ?? void 0,
550
+ flushInterval: 0,
551
+ serviceName: config.serviceName ?? stringFromEnv(env, "OTEL_SERVICE_NAME") ?? "unknown"
552
+ });
553
+ const make = (config = {}) => {
554
+ let handle;
555
+ const flush = makeSerializedFlush(guardFlush("[MxlCloudflareSDK]", async (env) => {
556
+ await new Promise((resolve) => setTimeout(resolve, 0));
557
+ const eventsConfig = resolveEventsConfig(env, config);
558
+ if (!eventsConfig.clientId) return;
559
+ handle ??= makeHandle(eventsConfig);
560
+ await handle.flush();
561
+ }, { disabledUntil: 0 }));
562
+ const layer$2 = layer({
563
+ attributes: Object.fromEntries(Object.entries(config.attributes ?? {}).map(([key, value]) => [key, String(value)])),
564
+ endpoint: config.endpoint,
565
+ environment: config.environment,
566
+ serviceName: config.serviceName,
567
+ serviceVersion: config.serviceVersion
568
+ });
569
+ return {
570
+ flush,
571
+ layer: layer$2,
572
+ requestLayer: Layer.mergeAll(layer$2, Layer.unwrap(Effect.gen(function* () {
573
+ const env = yield* WorkerEnvironment;
574
+ const eventsConfig = resolveEventsConfig(env, config);
575
+ if (!eventsConfig.clientId) return Layer.empty;
576
+ return layer$1(eventsConfig);
577
+ })), Layer.effectDiscard(Effect.gen(function* () {
578
+ const env = yield* WorkerEnvironment;
579
+ yield* Effect.addFinalizer(() => Effect.promise(() => flush(env)));
580
+ })))
581
+ };
582
+ };
583
+ const MxlCloudflareSDK = { make };
584
+ //#endregion
585
+ export { MxlCloudflareSDK, WorkerEnvironment, make };
586
+
587
+ //# sourceMappingURL=index.js.map