@krak-stack/registry 0.1.39 → 0.1.41

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.
@@ -1,21 +1,153 @@
1
1
  // ../../src/services/opentelemetry/browser.ts
2
- import { Effect, FiberSet, Layer, Predicate, Schema, Stream } from "effect";
2
+ import {
3
+ Context as Context2,
4
+ Effect as Effect2,
5
+ FiberSet,
6
+ Layer as Layer2,
7
+ Predicate,
8
+ Ref,
9
+ References as References2,
10
+ Schema as Schema3,
11
+ Stream
12
+ } from "effect";
3
13
  import { FetchHttpClient, HttpClientRequest } from "effect/http";
4
14
  import {
5
- OtlpExporter,
15
+ OtlpExporter as OtlpExporter2,
6
16
  OtlpLogger,
7
17
  OtlpMetrics,
8
- OtlpSerialization,
18
+ OtlpSerialization as OtlpSerialization2,
9
19
  OtlpTracer
10
20
  } from "effect/observability";
11
21
 
12
- class BrowserReportedError extends Schema.TaggedError()("BrowserReportedError", {
13
- type: Schema.Literals(["window.error", "unhandledrejection"]),
14
- message: Schema.String,
15
- stack: Schema.optional(Schema.String)
22
+ // ../../src/services/opentelemetry/events.ts
23
+ import {
24
+ Clock,
25
+ Context,
26
+ DateTime,
27
+ Effect,
28
+ Layer,
29
+ Option,
30
+ References,
31
+ Schema as Schema2
32
+ } from "effect";
33
+ import {
34
+ OtlpExporter,
35
+ OtlpResource,
36
+ OtlpSerialization
37
+ } from "effect/observability";
38
+
39
+ // ../../src/services/opentelemetry/schema.ts
40
+ import { Schema } from "effect";
41
+ var EventAttributeValue = Schema.Union([
42
+ Schema.String,
43
+ Schema.Finite,
44
+ Schema.Boolean,
45
+ Schema.Array(Schema.String),
46
+ Schema.Array(Schema.Finite),
47
+ Schema.Array(Schema.Boolean)
48
+ ]);
49
+ var EventAttributesSchema = Schema.Record(Schema.String, EventAttributeValue).annotate({ identifier: "EventAttributes" });
50
+ var EventOptionsSchema = Schema.Struct({
51
+ userId: Schema.optional(Schema.NonEmptyString),
52
+ anonymousId: Schema.optional(Schema.NonEmptyString),
53
+ sessionId: Schema.optional(Schema.NonEmptyString),
54
+ messageId: Schema.optional(Schema.NonEmptyString),
55
+ timestamp: Schema.optional(Schema.DateTimeUtc),
56
+ context: Schema.optional(EventAttributesSchema)
57
+ }).annotate({ identifier: "EventOptions" });
58
+ var TelemetryEvent = Schema.Struct({
59
+ name: Schema.NonEmptyString,
60
+ properties: Schema.optional(EventAttributesSchema),
61
+ options: Schema.optional(EventOptionsSchema)
62
+ }).annotate({ identifier: "TelemetryEvent" });
63
+ var PageEvent = Schema.Struct({
64
+ name: Schema.optional(Schema.NonEmptyString),
65
+ category: Schema.optional(Schema.NonEmptyString),
66
+ path: Schema.optional(Schema.NonEmptyString),
67
+ properties: Schema.optional(EventAttributesSchema),
68
+ options: Schema.optional(EventOptionsSchema)
69
+ }).annotate({ identifier: "PageEvent" });
70
+ var BrowserIdentity = Schema.Struct({
71
+ userId: Schema.optional(Schema.NonEmptyString),
72
+ organizationId: Schema.optional(Schema.NonEmptyString)
73
+ }).annotate({ identifier: "BrowserTelemetryIdentity" });
74
+
75
+ // ../../src/services/opentelemetry/events.ts
76
+ class OtlpEvents extends Context.Service()("krakstack/opentelemetry/Events") {
77
+ static disabledLayer = Layer.succeed(this, { push: () => {} });
78
+ static layer = (options) => Layer.effect(this, Effect.gen(function* () {
79
+ const serialization = yield* OtlpSerialization.OtlpSerialization;
80
+ const resource = yield* OtlpResource.fromConfig(options.resource);
81
+ return yield* OtlpExporter.make({
82
+ ...options,
83
+ label: "OtlpEvents",
84
+ body: (logRecords) => [
85
+ serialization.logs({
86
+ resourceLogs: [
87
+ {
88
+ resource,
89
+ scopeLogs: [
90
+ {
91
+ scope: { name: "krakstack.events" },
92
+ logRecords
93
+ }
94
+ ]
95
+ }
96
+ ]
97
+ }),
98
+ Effect.void
99
+ ]
100
+ });
101
+ }));
102
+ }
103
+ var track = Effect.fnUntraced(function* (name, properties, options) {
104
+ const event = yield* Schema2.decodeUnknownEffect(TelemetryEvent)({
105
+ name,
106
+ properties,
107
+ options
108
+ });
109
+ const events = yield* OtlpEvents;
110
+ const clock = yield* Clock.Clock;
111
+ const observedTime = clock.currentTimeNanosUnsafe().toString();
112
+ const timestamp = event.options?.timestamp ? (BigInt(DateTime.toEpochMillis(event.options.timestamp)) * 1000000n).toString() : observedTime;
113
+ const span = yield* Effect.currentSpan.pipe(Effect.option);
114
+ const attributes = { ...event.options?.context, ...event.properties };
115
+ if (event.options?.userId)
116
+ attributes["user.id"] = event.options.userId;
117
+ if (event.options?.anonymousId)
118
+ attributes["analytics.anonymous_id"] = event.options.anonymousId;
119
+ if (event.options?.sessionId)
120
+ attributes["session.id"] = event.options.sessionId;
121
+ if (event.options?.messageId)
122
+ attributes["log.record.uid"] = event.options.messageId;
123
+ const annotations = yield* References.CurrentLogAnnotations;
124
+ events.push({
125
+ eventName: event.name,
126
+ timeUnixNano: timestamp,
127
+ observedTimeUnixNano: observedTime,
128
+ severityNumber: 9,
129
+ attributes: OtlpResource.entriesToAttributes(Object.entries({
130
+ ...attributes,
131
+ ...annotations
132
+ })),
133
+ droppedAttributesCount: 0,
134
+ traceId: Option.isSome(span) ? span.value.traceId : undefined,
135
+ spanId: Option.isSome(span) ? span.value.spanId : undefined
136
+ });
137
+ });
138
+
139
+ // ../../src/services/opentelemetry/browser.ts
140
+ class BrowserIdentityState extends Context2.Service()("krakstack/opentelemetry/BrowserIdentity") {
141
+ static layer = Layer2.effect(this, Ref.make({}));
142
+ }
143
+
144
+ class BrowserReportedError extends Schema3.TaggedError()("BrowserReportedError", {
145
+ type: Schema3.Literals(["window.error", "unhandledrejection"]),
146
+ message: Schema3.String,
147
+ stack: Schema3.optional(Schema3.String)
16
148
  }) {
17
149
  }
18
- var reportBrowserException = (exception) => Effect.fail(exception).pipe(Effect.withSpan("browser.exception", {
150
+ var reportBrowserException = (exception) => Effect2.fail(exception).pipe(Effect2.withSpan("browser.exception", {
19
151
  root: true,
20
152
  sampled: true,
21
153
  attributes: {
@@ -23,8 +155,8 @@ var reportBrowserException = (exception) => Effect.fail(exception).pipe(Effect.w
23
155
  "exception.message": exception.message,
24
156
  "exception.stacktrace": exception.stack ?? ""
25
157
  }
26
- }), Effect.ignoreCause);
27
- var BrowserExceptionTrackingLive = Layer.effectDiscard(Stream.merge(Stream.fromEventListener(globalThis, "error", {
158
+ }), Effect2.ignoreCause);
159
+ var BrowserExceptionTrackingLive = Layer2.effectDiscard(Stream.merge(Stream.fromEventListener(globalThis, "error", {
28
160
  bufferSize: 100
29
161
  }).pipe(Stream.map((event) => {
30
162
  const error = event.error;
@@ -43,13 +175,13 @@ var BrowserExceptionTrackingLive = Layer.effectDiscard(Stream.merge(Stream.fromE
43
175
  type: "unhandledrejection",
44
176
  message: "Unhandled promise rejection"
45
177
  });
46
- }))).pipe(Stream.runForEach(reportBrowserException), Effect.forkScoped));
47
- var BrowserLifecycleFlushingLive = Layer.effectDiscard(Effect.gen(function* () {
48
- const flusher = yield* OtlpExporter.Flusher;
178
+ }))).pipe(Stream.runForEach(reportBrowserException), Effect2.forkScoped));
179
+ var BrowserLifecycleFlushingLive = Layer2.effectDiscard(Effect2.gen(function* () {
180
+ const flusher = yield* OtlpExporter2.Flusher;
49
181
  const fibers = yield* FiberSet.make();
50
182
  const run = yield* FiberSet.runtime(fibers)();
51
183
  const flush = () => {
52
- run(flusher.flush.pipe(Effect.provideService(FetchHttpClient.RequestInit, {
184
+ run(flusher.flush.pipe(Effect2.provideService(FetchHttpClient.RequestInit, {
53
185
  keepalive: true
54
186
  })));
55
187
  };
@@ -57,50 +189,164 @@ var BrowserLifecycleFlushingLive = Layer.effectDiscard(Effect.gen(function* () {
57
189
  if (document.visibilityState === "hidden")
58
190
  flush();
59
191
  };
60
- yield* Effect.acquireRelease(Effect.sync(() => {
192
+ yield* Effect2.acquireRelease(Effect2.sync(() => {
61
193
  document.addEventListener("visibilitychange", onVisibilityChange);
62
194
  globalThis.addEventListener("pagehide", flush);
63
- }), () => Effect.sync(() => {
195
+ }), () => Effect2.sync(() => {
64
196
  document.removeEventListener("visibilitychange", onVisibilityChange);
65
197
  globalThis.removeEventListener("pagehide", flush);
66
198
  }));
67
199
  }));
200
+ var browserEffects = {
201
+ identify: Effect2.fnUntraced(function* (input) {
202
+ const identity = yield* Schema3.decodeUnknownEffect(BrowserIdentity)(input === null ? {} : input);
203
+ const state = yield* BrowserIdentityState;
204
+ yield* Ref.set(state, identity);
205
+ }),
206
+ track: Effect2.fnUntraced(function* (name, properties, options) {
207
+ const state = yield* BrowserIdentityState;
208
+ const identity = yield* Ref.get(state);
209
+ const annotations = {};
210
+ if (identity.userId)
211
+ annotations["user.id"] = identity.userId;
212
+ if (identity.organizationId)
213
+ annotations["organization.id"] = identity.organizationId;
214
+ const scoped = yield* References2.CurrentLogAnnotations;
215
+ yield* track(name, properties, options).pipe(Effect2.annotateLogs({ ...annotations, ...scoped }));
216
+ }),
217
+ page: Effect2.fnUntraced(function* (input = {}) {
218
+ const page = yield* Schema3.decodeUnknownEffect(PageEvent)(input);
219
+ const properties = { ...page.properties };
220
+ if (page.name)
221
+ properties["analytics.page.name"] = page.name;
222
+ if (page.category)
223
+ properties["analytics.page.category"] = page.category;
224
+ properties["url.path"] = (page.path ?? globalThis.location.pathname).split(/[?#]/, 1)[0] ?? "/";
225
+ yield* browserEffects.track("browser.page_view", properties, page.options);
226
+ })
227
+ };
228
+ var browserClients = [];
229
+ var runBrowser = (program) => Effect2.suspend(() => {
230
+ const client = browserClients.at(-1);
231
+ if (!client)
232
+ return Effect2.void;
233
+ return program.pipe(Effect2.provideService(BrowserIdentityState, client.identity), Effect2.provideService(OtlpEvents, client.events));
234
+ });
235
+ var BrowserClientLive = Layer2.effectDiscard(Effect2.gen(function* () {
236
+ const identity = yield* BrowserIdentityState;
237
+ const events = yield* OtlpEvents;
238
+ yield* Effect2.acquireRelease(Effect2.sync(() => {
239
+ const client = { identity, events };
240
+ browserClients.push(client);
241
+ return client;
242
+ }), (client) => Effect2.sync(() => {
243
+ const index = browserClients.indexOf(client);
244
+ if (index !== -1)
245
+ browserClients.splice(index, 1);
246
+ }));
247
+ }));
248
+ var pageViewListeners = new Set;
249
+ var stopPageViewTracking;
250
+ var subscribePageViews = (listener) => {
251
+ if (pageViewListeners.size === 0) {
252
+ const history = globalThis.history;
253
+ const pushState = history.pushState;
254
+ const replaceState = history.replaceState;
255
+ const notify = () => {
256
+ for (const onPage of pageViewListeners)
257
+ onPage();
258
+ };
259
+ const trackedPushState = (...args) => {
260
+ pushState.apply(history, args);
261
+ notify();
262
+ };
263
+ const trackedReplaceState = (...args) => {
264
+ replaceState.apply(history, args);
265
+ notify();
266
+ };
267
+ history.pushState = trackedPushState;
268
+ history.replaceState = trackedReplaceState;
269
+ globalThis.addEventListener("popstate", notify);
270
+ stopPageViewTracking = () => {
271
+ globalThis.removeEventListener("popstate", notify);
272
+ if (history.pushState === trackedPushState)
273
+ history.pushState = pushState;
274
+ if (history.replaceState === trackedReplaceState)
275
+ history.replaceState = replaceState;
276
+ };
277
+ }
278
+ pageViewListeners.add(listener);
279
+ return () => {
280
+ pageViewListeners.delete(listener);
281
+ if (pageViewListeners.size === 0) {
282
+ stopPageViewTracking?.();
283
+ stopPageViewTracking = undefined;
284
+ }
285
+ };
286
+ };
287
+ var BrowserPageViewsLive = Layer2.effectDiscard(Effect2.gen(function* () {
288
+ const fibers = yield* FiberSet.make();
289
+ const run = yield* FiberSet.runtime(fibers)();
290
+ let previousPath = globalThis.location.pathname;
291
+ yield* Effect2.acquireRelease(Effect2.sync(() => subscribePageViews(() => {
292
+ const path = globalThis.location.pathname;
293
+ if (path === previousPath)
294
+ return;
295
+ previousPath = path;
296
+ run(browserEffects.page({ path }).pipe(Effect2.ignore));
297
+ })), (unsubscribe) => Effect2.sync(unsubscribe));
298
+ yield* browserEffects.page({ path: previousPath }).pipe(Effect2.ignore);
299
+ }));
68
300
 
69
301
  class BrowserOtlp {
302
+ static identify = (identity) => runBrowser(browserEffects.identify(identity)).pipe(Effect2.catch(() => Effect2.logWarning("Invalid browser telemetry identity; keeping previous identity")));
303
+ static track = (name, properties, options) => runBrowser(browserEffects.track(name, properties, options));
304
+ static page = (options) => runBrowser(browserEffects.page(options));
305
+ static pageView = BrowserOtlp.page;
70
306
  static layer = ({
71
307
  attributes,
72
308
  baseUrl = "/api/otel",
73
309
  logs = false,
310
+ events = false,
74
311
  metrics = false,
75
312
  serviceName,
76
313
  serviceVersion,
77
314
  traces = {},
78
315
  headers
79
316
  }) => {
317
+ const { pageViews = true, ...eventOptions } = events === false ? {} : events;
80
318
  const resource = { attributes, serviceName, serviceVersion };
81
319
  const request = HttpClientRequest.get(baseUrl);
82
320
  const signalUrl = (signal) => HttpClientRequest.appendUrl(request, `/v1/${signal}`).url;
83
- const exporters = Layer.mergeAll(OtlpExporter.layerFlusher, traces !== false ? OtlpTracer.layer({
321
+ const exporters = Layer2.mergeAll(OtlpExporter2.layerFlusher, events !== false ? OtlpEvents.layer({
322
+ exportInterval: "30 seconds",
323
+ maxBatchSize: 100,
324
+ shutdownTimeout: "3 seconds",
325
+ ...eventOptions,
326
+ url: signalUrl("logs"),
327
+ resource,
328
+ headers
329
+ }) : OtlpEvents.disabledLayer, traces !== false ? OtlpTracer.layer({
84
330
  exportInterval: "30 seconds",
85
331
  ...traces,
86
332
  url: signalUrl("traces"),
87
333
  resource,
88
334
  headers
89
- }) : Layer.empty, metrics !== false ? OtlpMetrics.layer({
335
+ }) : Layer2.empty, metrics !== false ? OtlpMetrics.layer({
90
336
  exportInterval: "60 seconds",
91
337
  ...metrics,
92
338
  url: signalUrl("metrics"),
93
339
  resource,
94
340
  headers
95
- }) : Layer.empty, logs !== false ? OtlpLogger.layer({
341
+ }) : Layer2.empty, logs !== false ? OtlpLogger.layer({
96
342
  excludeLogSpans: true,
97
343
  mergeWithExisting: true,
98
344
  ...logs,
99
345
  url: signalUrl("logs"),
100
346
  resource,
101
347
  headers
102
- }) : Layer.empty).pipe(Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer));
103
- return Layer.mergeAll(BrowserLifecycleFlushingLive, traces !== false ? BrowserExceptionTrackingLive : Layer.empty).pipe(Layer.provideMerge(exporters));
348
+ }) : Layer2.empty).pipe(Layer2.provideMerge(OtlpExporter2.layerFlusher), Layer2.provide(OtlpSerialization2.layerJson), Layer2.provide(FetchHttpClient.layer));
349
+ return Layer2.mergeAll(BrowserClientLive, BrowserLifecycleFlushingLive, events !== false && pageViews ? BrowserPageViewsLive : Layer2.empty, traces !== false ? BrowserExceptionTrackingLive : Layer2.empty).pipe(Layer2.provideMerge(Layer2.merge(BrowserIdentityState.layer, exporters)));
104
350
  };
105
351
  }
106
352
  export {
@@ -0,0 +1,37 @@
1
+ import { Context, DateTime, Effect, Layer, Schema } from "effect";
2
+ import { OtlpExporter, OtlpResource, OtlpSerialization } from "effect/observability";
3
+ type ExporterOptions = Pick<Parameters<typeof OtlpExporter.make>[0], "url" | "headers" | "exportInterval" | "maxBatchSize" | "shutdownTimeout"> & {
4
+ readonly resource?: Parameters<typeof OtlpResource.fromConfig>[0];
5
+ };
6
+ export type EventExportOptions = Partial<Pick<ExporterOptions, "exportInterval" | "maxBatchSize" | "shutdownTimeout">>;
7
+ declare const OtlpEvents_base: Context.ServiceClass<OtlpEvents, "krakstack/opentelemetry/Events", {
8
+ readonly push: (record: EventRecord) => void;
9
+ }>;
10
+ export declare class OtlpEvents extends OtlpEvents_base {
11
+ static readonly disabledLayer: Layer.Layer<OtlpEvents, never, never>;
12
+ static readonly layer: (options: ExporterOptions) => Layer.Layer<OtlpEvents, never, OtlpExporter.Flusher | import("effect/http/HttpClient").HttpClient | OtlpSerialization.OtlpSerialization>;
13
+ }
14
+ interface EventRecord {
15
+ readonly eventName: string;
16
+ readonly timeUnixNano: string;
17
+ readonly observedTimeUnixNano: string;
18
+ readonly severityNumber: 9;
19
+ readonly attributes: ReturnType<typeof OtlpResource.entriesToAttributes>;
20
+ readonly droppedAttributesCount: 0;
21
+ readonly traceId?: string;
22
+ readonly spanId?: string;
23
+ }
24
+ /** Emits an INFO event without creating a tracing span or persisting identity. */
25
+ export declare const track: (name: string, properties?: {
26
+ readonly [x: string]: string | number | boolean | readonly string[] | readonly number[] | readonly boolean[];
27
+ } | undefined, options?: {
28
+ readonly userId?: string | undefined;
29
+ readonly anonymousId?: string | undefined;
30
+ readonly sessionId?: string | undefined;
31
+ readonly messageId?: string | undefined;
32
+ readonly timestamp?: DateTime.Utc | undefined;
33
+ readonly context?: {
34
+ readonly [x: string]: string | number | boolean | readonly string[] | readonly number[] | readonly boolean[];
35
+ } | undefined;
36
+ } | undefined) => Effect.Effect<void, Schema.SchemaError, OtlpEvents>;
37
+ export {};
@@ -0,0 +1,44 @@
1
+ import { Schema } from "effect";
2
+ export declare const EventAttributesSchema: Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Finite, Schema.Boolean, Schema.$Array<Schema.String>, Schema.$Array<Schema.Finite>, Schema.$Array<Schema.Boolean>]>>;
3
+ export declare const EventOptionsSchema: Schema.Struct<{
4
+ readonly userId: Schema.optional<Schema.NonEmptyString>;
5
+ readonly anonymousId: Schema.optional<Schema.NonEmptyString>;
6
+ readonly sessionId: Schema.optional<Schema.NonEmptyString>;
7
+ readonly messageId: Schema.optional<Schema.NonEmptyString>;
8
+ readonly timestamp: Schema.optional<Schema.DateTimeUtc>;
9
+ readonly context: Schema.optional<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Finite, Schema.Boolean, Schema.$Array<Schema.String>, Schema.$Array<Schema.Finite>, Schema.$Array<Schema.Boolean>]>>>;
10
+ }>;
11
+ export declare const TelemetryEvent: Schema.Struct<{
12
+ readonly name: Schema.NonEmptyString;
13
+ readonly properties: Schema.optional<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Finite, Schema.Boolean, Schema.$Array<Schema.String>, Schema.$Array<Schema.Finite>, Schema.$Array<Schema.Boolean>]>>>;
14
+ readonly options: Schema.optional<Schema.Struct<{
15
+ readonly userId: Schema.optional<Schema.NonEmptyString>;
16
+ readonly anonymousId: Schema.optional<Schema.NonEmptyString>;
17
+ readonly sessionId: Schema.optional<Schema.NonEmptyString>;
18
+ readonly messageId: Schema.optional<Schema.NonEmptyString>;
19
+ readonly timestamp: Schema.optional<Schema.DateTimeUtc>;
20
+ readonly context: Schema.optional<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Finite, Schema.Boolean, Schema.$Array<Schema.String>, Schema.$Array<Schema.Finite>, Schema.$Array<Schema.Boolean>]>>>;
21
+ }>>;
22
+ }>;
23
+ export declare const PageEvent: Schema.Struct<{
24
+ readonly name: Schema.optional<Schema.NonEmptyString>;
25
+ readonly category: Schema.optional<Schema.NonEmptyString>;
26
+ readonly path: Schema.optional<Schema.NonEmptyString>;
27
+ readonly properties: Schema.optional<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Finite, Schema.Boolean, Schema.$Array<Schema.String>, Schema.$Array<Schema.Finite>, Schema.$Array<Schema.Boolean>]>>>;
28
+ readonly options: Schema.optional<Schema.Struct<{
29
+ readonly userId: Schema.optional<Schema.NonEmptyString>;
30
+ readonly anonymousId: Schema.optional<Schema.NonEmptyString>;
31
+ readonly sessionId: Schema.optional<Schema.NonEmptyString>;
32
+ readonly messageId: Schema.optional<Schema.NonEmptyString>;
33
+ readonly timestamp: Schema.optional<Schema.DateTimeUtc>;
34
+ readonly context: Schema.optional<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Finite, Schema.Boolean, Schema.$Array<Schema.String>, Schema.$Array<Schema.Finite>, Schema.$Array<Schema.Boolean>]>>>;
35
+ }>>;
36
+ }>;
37
+ export type EventAttributes = typeof EventAttributesSchema.Type;
38
+ export type EventOptions = typeof EventOptionsSchema.Type;
39
+ export type PageOptions = typeof PageEvent.Type;
40
+ export declare const BrowserIdentity: Schema.Struct<{
41
+ readonly userId: Schema.optional<Schema.NonEmptyString>;
42
+ readonly organizationId: Schema.optional<Schema.NonEmptyString>;
43
+ }>;
44
+ export type BrowserIdentity = typeof BrowserIdentity.Type;
@@ -0,0 +1,20 @@
1
+ import { Effect, Layer, Schema } from "effect";
2
+ import { OtlpExporter } from "effect/observability";
3
+ import { OtlpEvents } from "./events.js";
4
+ export type { EventAttributes, EventOptions } from "./schema.js";
5
+ /** Runtime-neutral server telemetry for HTTP applications, workers, and CLIs. */
6
+ export declare class ServerOtlp {
7
+ static readonly track: (name: string, properties?: {
8
+ readonly [x: string]: string | number | boolean | readonly string[] | readonly number[] | readonly boolean[];
9
+ } | undefined, options?: {
10
+ readonly userId?: string | undefined;
11
+ readonly anonymousId?: string | undefined;
12
+ readonly sessionId?: string | undefined;
13
+ readonly messageId?: string | undefined;
14
+ readonly timestamp?: import("effect/DateTime").Utc | undefined;
15
+ readonly context?: {
16
+ readonly [x: string]: string | number | boolean | readonly string[] | readonly number[] | readonly boolean[];
17
+ } | undefined;
18
+ } | undefined) => Effect.Effect<void, Schema.SchemaError, OtlpEvents>;
19
+ static readonly layer: Layer.Layer<OtlpExporter.Flusher | OtlpEvents, never, never>;
20
+ }
@@ -0,0 +1,154 @@
1
+ // ../../src/services/opentelemetry/server.ts
2
+ import { Config, Effect as Effect2, Layer as Layer2, Schema as Schema3 } from "effect";
3
+ import { FetchHttpClient } from "effect/http";
4
+ import { Otlp, OtlpExporter as OtlpExporter2, OtlpSerialization as OtlpSerialization2 } from "effect/observability";
5
+
6
+ // ../../src/services/opentelemetry/events.ts
7
+ import {
8
+ Clock,
9
+ Context,
10
+ DateTime,
11
+ Effect,
12
+ Layer,
13
+ Option,
14
+ References,
15
+ Schema as Schema2
16
+ } from "effect";
17
+ import {
18
+ OtlpExporter,
19
+ OtlpResource,
20
+ OtlpSerialization
21
+ } from "effect/observability";
22
+
23
+ // ../../src/services/opentelemetry/schema.ts
24
+ import { Schema } from "effect";
25
+ var EventAttributeValue = Schema.Union([
26
+ Schema.String,
27
+ Schema.Finite,
28
+ Schema.Boolean,
29
+ Schema.Array(Schema.String),
30
+ Schema.Array(Schema.Finite),
31
+ Schema.Array(Schema.Boolean)
32
+ ]);
33
+ var EventAttributesSchema = Schema.Record(Schema.String, EventAttributeValue).annotate({ identifier: "EventAttributes" });
34
+ var EventOptionsSchema = Schema.Struct({
35
+ userId: Schema.optional(Schema.NonEmptyString),
36
+ anonymousId: Schema.optional(Schema.NonEmptyString),
37
+ sessionId: Schema.optional(Schema.NonEmptyString),
38
+ messageId: Schema.optional(Schema.NonEmptyString),
39
+ timestamp: Schema.optional(Schema.DateTimeUtc),
40
+ context: Schema.optional(EventAttributesSchema)
41
+ }).annotate({ identifier: "EventOptions" });
42
+ var TelemetryEvent = Schema.Struct({
43
+ name: Schema.NonEmptyString,
44
+ properties: Schema.optional(EventAttributesSchema),
45
+ options: Schema.optional(EventOptionsSchema)
46
+ }).annotate({ identifier: "TelemetryEvent" });
47
+ var PageEvent = Schema.Struct({
48
+ name: Schema.optional(Schema.NonEmptyString),
49
+ category: Schema.optional(Schema.NonEmptyString),
50
+ path: Schema.optional(Schema.NonEmptyString),
51
+ properties: Schema.optional(EventAttributesSchema),
52
+ options: Schema.optional(EventOptionsSchema)
53
+ }).annotate({ identifier: "PageEvent" });
54
+ var BrowserIdentity = Schema.Struct({
55
+ userId: Schema.optional(Schema.NonEmptyString),
56
+ organizationId: Schema.optional(Schema.NonEmptyString)
57
+ }).annotate({ identifier: "BrowserTelemetryIdentity" });
58
+
59
+ // ../../src/services/opentelemetry/events.ts
60
+ class OtlpEvents extends Context.Service()("krakstack/opentelemetry/Events") {
61
+ static disabledLayer = Layer.succeed(this, { push: () => {} });
62
+ static layer = (options) => Layer.effect(this, Effect.gen(function* () {
63
+ const serialization = yield* OtlpSerialization.OtlpSerialization;
64
+ const resource = yield* OtlpResource.fromConfig(options.resource);
65
+ return yield* OtlpExporter.make({
66
+ ...options,
67
+ label: "OtlpEvents",
68
+ body: (logRecords) => [
69
+ serialization.logs({
70
+ resourceLogs: [
71
+ {
72
+ resource,
73
+ scopeLogs: [
74
+ {
75
+ scope: { name: "krakstack.events" },
76
+ logRecords
77
+ }
78
+ ]
79
+ }
80
+ ]
81
+ }),
82
+ Effect.void
83
+ ]
84
+ });
85
+ }));
86
+ }
87
+ var track = Effect.fnUntraced(function* (name, properties, options) {
88
+ const event = yield* Schema2.decodeUnknownEffect(TelemetryEvent)({
89
+ name,
90
+ properties,
91
+ options
92
+ });
93
+ const events = yield* OtlpEvents;
94
+ const clock = yield* Clock.Clock;
95
+ const observedTime = clock.currentTimeNanosUnsafe().toString();
96
+ const timestamp = event.options?.timestamp ? (BigInt(DateTime.toEpochMillis(event.options.timestamp)) * 1000000n).toString() : observedTime;
97
+ const span = yield* Effect.currentSpan.pipe(Effect.option);
98
+ const attributes = { ...event.options?.context, ...event.properties };
99
+ if (event.options?.userId)
100
+ attributes["user.id"] = event.options.userId;
101
+ if (event.options?.anonymousId)
102
+ attributes["analytics.anonymous_id"] = event.options.anonymousId;
103
+ if (event.options?.sessionId)
104
+ attributes["session.id"] = event.options.sessionId;
105
+ if (event.options?.messageId)
106
+ attributes["log.record.uid"] = event.options.messageId;
107
+ const annotations = yield* References.CurrentLogAnnotations;
108
+ events.push({
109
+ eventName: event.name,
110
+ timeUnixNano: timestamp,
111
+ observedTimeUnixNano: observedTime,
112
+ severityNumber: 9,
113
+ attributes: OtlpResource.entriesToAttributes(Object.entries({
114
+ ...attributes,
115
+ ...annotations
116
+ })),
117
+ droppedAttributesCount: 0,
118
+ traceId: Option.isSome(span) ? span.value.traceId : undefined,
119
+ spanId: Option.isSome(span) ? span.value.spanId : undefined
120
+ });
121
+ });
122
+
123
+ // ../../src/services/opentelemetry/server.ts
124
+ var ServerEventsLive = Layer2.unwrap(Effect2.gen(function* () {
125
+ const disabled = yield* Config.Boolean("OTEL_SDK_DISABLED").pipe(Config.withDefault(false));
126
+ const exporters = yield* Config.Array(Schema3.String, "OTEL_LOGS_EXPORTER").pipe(Config.withDefault([]));
127
+ if (disabled || !exporters.some((value) => value.trim().toLowerCase() === "otlp")) {
128
+ return OtlpEvents.disabledLayer;
129
+ }
130
+ const endpoint = yield* Config.URL("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT").pipe(Config.orElse(() => Config.URL("OTEL_EXPORTER_OTLP_ENDPOINT").pipe(Config.map((url) => {
131
+ url.pathname += `${url.pathname.endsWith("/") ? "" : "/"}v1/logs`;
132
+ return url;
133
+ }))), Config.withDefault(undefined));
134
+ if (!endpoint) {
135
+ return OtlpEvents.disabledLayer;
136
+ }
137
+ const headersConfig = (name) => Config.Record(Schema3.String, Schema3.StringFromUriComponent, name);
138
+ const headers = yield* headersConfig("OTEL_EXPORTER_OTLP_LOGS_HEADERS").pipe(Config.orElse(() => headersConfig("OTEL_EXPORTER_OTLP_HEADERS")), Config.withDefault(undefined));
139
+ return OtlpEvents.layer({
140
+ url: endpoint.toString(),
141
+ headers,
142
+ exportInterval: "1 second",
143
+ maxBatchSize: 1000,
144
+ shutdownTimeout: "3 seconds"
145
+ });
146
+ }).pipe(Effect2.orDie));
147
+
148
+ class ServerOtlp {
149
+ static track = track;
150
+ static layer = Layer2.mergeAll(Otlp.layerFromConfig(), ServerEventsLive).pipe(Layer2.provideMerge(OtlpExporter2.layerFlusher), Layer2.provide(OtlpSerialization2.layerJson), Layer2.provide(FetchHttpClient.layer));
151
+ }
152
+ export {
153
+ ServerOtlp
154
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krak-stack/registry",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "Tree-shakable KrakStack components and Effect services.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -49,6 +49,10 @@
49
49
  "types": "./dist/services/opentelemetry/browser.d.ts",
50
50
  "import": "./dist/services/opentelemetry/browser.js"
51
51
  },
52
+ "./opentelemetry/server": {
53
+ "types": "./dist/services/opentelemetry/server.d.ts",
54
+ "import": "./dist/services/opentelemetry/server.js"
55
+ },
52
56
  "./docs": {
53
57
  "types": "./dist/lib/docs.d.ts",
54
58
  "import": "./dist/lib/docs.js"