@arcote.tech/arc-host 0.7.5 → 0.7.7

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,17 +1,19 @@
1
1
  import { type ArcContextAny, type ArcEventAny, type DatabaseAdapter, LocalEventPublisher, MasterDataStorage, Model } from "@arcote.tech/arc";
2
+ import type { ArcTelemetry } from "@arcote.tech/arc-otel";
2
3
  import type { EventAuthContext, SyncableEvent, TokenPayload } from "./types";
3
4
  /**
4
5
  * Handles a single context on the host
5
6
  */
6
7
  export declare class ContextHandler {
7
8
  readonly context: ArcContextAny;
9
+ private readonly telemetry?;
8
10
  private model;
9
11
  private dataStorage;
10
12
  private eventPublisher;
11
13
  private eventDefinitions;
12
14
  private hostEventIdCounter;
13
15
  private initialized;
14
- constructor(context: ArcContextAny, dbAdapter: Promise<DatabaseAdapter>);
16
+ constructor(context: ArcContextAny, dbAdapter: Promise<DatabaseAdapter>, telemetry?: ArcTelemetry | undefined);
15
17
  /**
16
18
  * Initialize the context handler and run seed data if needed.
17
19
  */
@@ -22,7 +24,9 @@ export declare class ContextHandler {
22
24
  */
23
25
  private runSeeds;
24
26
  /**
25
- * Execute a command
27
+ * Execute a command. When `telemetry` is wired, the entire dispatch is
28
+ * traced as `command.<name>` with RPC semantic conventions, parented to
29
+ * whatever context is active (HTTP request span, WS message span, etc.).
26
30
  */
27
31
  executeCommand(commandName: string, params: any, rawToken: string | null): Promise<any>;
28
32
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"context-handler.d.ts","sourceRoot":"","sources":["../../src/context-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,EAGN,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE7E;;GAEG;AACH,qBAAa,cAAc;aASP,OAAO,EAAE,aAAa;IARxC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,cAAc,CAAsB;IAC5C,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,WAAW,CAAS;gBAGV,OAAO,EAAE,aAAa,EACtC,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC;IAqBrC;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B;;;OAGG;YACW,QAAQ;IA4BtB;;OAEG;IACG,cAAc,CAClB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,GAAG,EACX,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,GAAG,CAAC;IA8Bf;;OAEG;IACG,aAAa,CACjB,MAAM,EAAE,KAAK,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,GAAG,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;KACvC,CAAC,EACF,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,YAAY,GAAG,IAAI,GACzB,OAAO,CAAC,aAAa,EAAE,CAAC;IA8C3B;;OAEG;IACG,cAAc,CAClB,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,KAAK,EAAE,YAAY,GAAG,IAAI,GACzB,OAAO,CAAC,aAAa,EAAE,CAAC;IAkD3B;;OAEG;IACH,QAAQ,IAAI,KAAK,CAAC,aAAa,CAAC;IAIhC;;OAEG;IACH,cAAc,IAAI,iBAAiB;IAInC;;OAEG;IACH,mBAAmB,IAAI,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC;IAI/C;;OAEG;IACH,iBAAiB,IAAI,mBAAmB;CAGzC"}
1
+ {"version":3,"file":"context-handler.d.ts","sourceRoot":"","sources":["../../src/context-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,EAGN,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE7E;;GAEG;AACH,qBAAa,cAAc;aASP,OAAO,EAAE,aAAa;IAEtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAV7B,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,cAAc,CAAsB;IAC5C,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,WAAW,CAAS;gBAGV,OAAO,EAAE,aAAa,EACtC,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC,EAClB,SAAS,CAAC,EAAE,YAAY,YAAA;IAqB3C;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B;;;OAGG;YACW,QAAQ;IA4BtB;;;;OAIG;IACG,cAAc,CAClB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,GAAG,EACX,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,GAAG,CAAC;IAuDf;;OAEG;IACG,aAAa,CACjB,MAAM,EAAE,KAAK,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,GAAG,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;KACvC,CAAC,EACF,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,YAAY,GAAG,IAAI,GACzB,OAAO,CAAC,aAAa,EAAE,CAAC;IA8C3B;;OAEG;IACG,cAAc,CAClB,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,KAAK,EAAE,YAAY,GAAG,IAAI,GACzB,OAAO,CAAC,aAAa,EAAE,CAAC;IAkD3B;;OAEG;IACH,QAAQ,IAAI,KAAK,CAAC,aAAa,CAAC;IAIhC;;OAEG;IACH,cAAc,IAAI,iBAAiB;IAInC;;OAEG;IACH,mBAAmB,IAAI,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC;IAI/C;;OAEG;IACH,iBAAiB,IAAI,mBAAmB;CAGzC"}
@@ -1,4 +1,5 @@
1
1
  import type { ArcContextAny, DatabaseAdapter } from "@arcote.tech/arc";
2
+ import type { ArcTelemetry } from "@arcote.tech/arc-otel";
2
3
  import type { Server } from "bun";
3
4
  import { ConnectionManager } from "./connection-manager";
4
5
  import { ContextHandler } from "./context-handler";
@@ -16,6 +17,11 @@ export interface ArcServerConfig {
16
17
  jwtSecret?: string;
17
18
  /** Extra callback when a WS client disconnects (e.g. to clean up platform state) */
18
19
  onWsClose?: (clientId: string) => void;
20
+ /** Optional OpenTelemetry instance. When provided, the HTTP fetch handler
21
+ * and WS message dispatch are wrapped in spans, with parent context
22
+ * extracted from `traceparent` (header / message field). All nested
23
+ * handler code automatically attaches to the active span. */
24
+ telemetry?: ArcTelemetry;
19
25
  }
20
26
  export interface ArcServer {
21
27
  server: Server<WebSocketData>;
@@ -1 +1 @@
1
- {"version":3,"file":"create-server.d.ts","sourceRoot":"","sources":["../../src/create-server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAElC,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAOjD,OAAO,KAAK,EACV,cAAc,EAGd,YAAY,EACb,MAAM,oBAAoB,CAAC;AAG5B,KAAK,aAAa,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,aAAa,CAAC;IACvB,gBAAgB,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IACzD,YAAY,CAAC,EAAE,cAAc,EAAE,CAAC;IAChC,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;IAC9B,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,aAAa,EAAE,aAAa,CAAC;IAC7B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,SAAS,CAAC,CAwLpB"}
1
+ {"version":3,"file":"create-server.d.ts","sourceRoot":"","sources":["../../src/create-server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAQ,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAElC,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAOjD,OAAO,KAAK,EACV,cAAc,EAGd,YAAY,EACb,MAAM,oBAAoB,CAAC;AAG5B,KAAK,aAAa,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,aAAa,CAAC;IACvB,gBAAgB,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IACzD,YAAY,CAAC,EAAE,cAAc,EAAE,CAAC;IAChC,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC;;;kEAG8D;IAC9D,SAAS,CAAC,EAAE,YAAY,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;IAC9B,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,aAAa,EAAE,aAAa,CAAC;IAC7B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,SAAS,CAAC,CA0PpB"}
package/package.json CHANGED
@@ -4,11 +4,11 @@
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
7
- "version": "0.7.5",
7
+ "version": "0.7.7",
8
8
  "private": false,
9
9
  "author": "Przemysław Krasiński [arcote.tech]",
10
10
  "dependencies": {
11
- "@arcote.tech/arc-adapter-db-sqlite": "^0.7.5",
11
+ "@arcote.tech/arc-adapter-db-sqlite": "^0.7.7",
12
12
  "croner": "^9.0.0",
13
13
  "jsonwebtoken": "^9.0.2"
14
14
  },
@@ -24,6 +24,12 @@
24
24
  "@types/bun": "^1.2.0"
25
25
  },
26
26
  "peerDependencies": {
27
- "@arcote.tech/arc": "^0.7.5"
27
+ "@arcote.tech/arc": "^0.7.7",
28
+ "@arcote.tech/arc-otel": "^0.7.7"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "@arcote.tech/arc-otel": {
32
+ "optional": true
33
+ }
28
34
  }
29
35
  }
@@ -8,6 +8,7 @@ import {
8
8
  ScopedModel,
9
9
  mutationExecutor,
10
10
  } from "@arcote.tech/arc";
11
+ import type { ArcTelemetry } from "@arcote.tech/arc-otel";
11
12
  import { canTokenEmitEvent, filterEventsForToken } from "./event-auth";
12
13
  import type { EventAuthContext, SyncableEvent, TokenPayload } from "./types";
13
14
 
@@ -25,6 +26,7 @@ export class ContextHandler {
25
26
  constructor(
26
27
  public readonly context: ArcContextAny,
27
28
  dbAdapter: Promise<DatabaseAdapter>,
29
+ private readonly telemetry?: ArcTelemetry,
28
30
  ) {
29
31
  this.dataStorage = new MasterDataStorage(dbAdapter);
30
32
  this.eventPublisher = new LocalEventPublisher(this.dataStorage);
@@ -88,39 +90,66 @@ export class ContextHandler {
88
90
  }
89
91
 
90
92
  /**
91
- * Execute a command
93
+ * Execute a command. When `telemetry` is wired, the entire dispatch is
94
+ * traced as `command.<name>` with RPC semantic conventions, parented to
95
+ * whatever context is active (HTTP request span, WS message span, etc.).
92
96
  */
93
97
  async executeCommand(
94
98
  commandName: string,
95
99
  params: any,
96
100
  rawToken: string | null,
97
101
  ): Promise<any> {
98
- // Create per-request scoped model with its own auth
99
- const scoped = new ScopedModel(this.model, "request");
100
- if (rawToken) scoped.setToken(rawToken);
101
-
102
- const mutations = mutationExecutor(scoped);
103
-
104
- // Support dotted names: "accounts.signIn" mutations.accounts.signIn
105
- let command: any;
106
- if (commandName.includes(".")) {
107
- const [elementName, methodName] = commandName.split(".");
108
- const element = (mutations as any)[elementName];
109
- command = element?.[methodName];
110
- } else {
111
- command = (mutations as any)[commandName];
112
- }
102
+ const includePayloads = this.telemetry?.shouldIncludePayloads() ?? false;
103
+ const baseAttrs = {
104
+ "rpc.system": "arc",
105
+ "rpc.method": commandName,
106
+ "arc.command.name": commandName,
107
+ "arc.command.params_size": params ? JSON.stringify(params).length : 0,
108
+ ...(includePayloads ? { "arc.command.params": params } : {}),
109
+ };
110
+
111
+ const runCommand = async (): Promise<any> => {
112
+ const scoped = new ScopedModel(this.model, "request");
113
+ if (rawToken) scoped.setToken(rawToken);
114
+
115
+ const mutations = mutationExecutor(scoped);
116
+
117
+ let command: any;
118
+ if (commandName.includes(".")) {
119
+ const [elementName, methodName] = commandName.split(".");
120
+ const element = (mutations as any)[elementName];
121
+ command = element?.[methodName];
122
+ } else {
123
+ command = (mutations as any)[commandName];
124
+ }
113
125
 
114
- if (!command) {
115
- throw new Error(`Command '${commandName}' not found`);
116
- }
126
+ if (!command) {
127
+ throw new Error(`Command '${commandName}' not found`);
128
+ }
117
129
 
130
+ try {
131
+ return await command(params);
132
+ } catch (error) {
133
+ console.error(`[ARC] Command '${commandName}' failed:`, error);
134
+ throw error;
135
+ }
136
+ };
137
+
138
+ if (!this.telemetry) return runCommand();
139
+ const start = Date.now();
118
140
  try {
119
- const result = await command(params);
120
- return result;
121
- } catch (error) {
122
- console.error(`[ARC] Command '${commandName}' failed:`, error);
123
- throw error;
141
+ return await this.telemetry.startSpan(
142
+ `command.${commandName}`,
143
+ runCommand,
144
+ { attributes: baseAttrs },
145
+ );
146
+ } finally {
147
+ this.telemetry.measureSince("arc.command.duration_ms", start, {
148
+ "arc.command.name": commandName,
149
+ });
150
+ this.telemetry.incrementCounter("arc.commands.total", 1, {
151
+ "arc.command.name": commandName,
152
+ });
124
153
  }
125
154
  }
126
155
 
@@ -1,4 +1,5 @@
1
1
  import type { ArcContextAny, DatabaseAdapter } from "@arcote.tech/arc";
2
+ import type { ArcTelemetry, Span } from "@arcote.tech/arc-otel";
2
3
  import type { Server } from "bun";
3
4
  import jwt from "jsonwebtoken";
4
5
  import { ConnectionManager } from "./connection-manager";
@@ -29,6 +30,11 @@ export interface ArcServerConfig {
29
30
  jwtSecret?: string;
30
31
  /** Extra callback when a WS client disconnects (e.g. to clean up platform state) */
31
32
  onWsClose?: (clientId: string) => void;
33
+ /** Optional OpenTelemetry instance. When provided, the HTTP fetch handler
34
+ * and WS message dispatch are wrapped in spans, with parent context
35
+ * extracted from `traceparent` (header / message field). All nested
36
+ * handler code automatically attaches to the active span. */
37
+ telemetry?: ArcTelemetry;
32
38
  }
33
39
 
34
40
  export interface ArcServer {
@@ -55,9 +61,21 @@ export async function createArcServer(
55
61
  "arc-host-secret-change-in-production";
56
62
  const port = config.port || 5005;
57
63
 
58
- // Init context handler
64
+ // Init context handler — telemetry (if any) flows through so executeCommand
65
+ // emits `command.<name>` spans parented to the active HTTP/WS span.
66
+ // DB adapter wrapping (per-transaction spans) is the caller's
67
+ // responsibility: pass an already-wrapped `dbAdapterFactory` if
68
+ // db spans are wanted. We don't `import()` arc-otel from here because
69
+ // doing so bundles its OTel SDK transitively into arc-host's dist,
70
+ // which then duplicates the @opentelemetry/api singleton when the CLI
71
+ // bundles arc-otel directly — leading to two global tracer providers
72
+ // and silent span loss.
59
73
  const dbAdapter = config.dbAdapterFactory(config.context);
60
- const contextHandler = new ContextHandler(config.context, dbAdapter);
74
+ const contextHandler = new ContextHandler(
75
+ config.context,
76
+ dbAdapter,
77
+ config.telemetry,
78
+ );
61
79
  await contextHandler.init();
62
80
 
63
81
  // Start cron scheduler
@@ -136,58 +154,74 @@ export async function createArcServer(
136
154
 
137
155
  async fetch(req, server) {
138
156
  const url = new URL(req.url);
139
-
140
- // Build CORS headers with request origin
141
157
  const corsHeaders = buildCorsHeaders(req);
142
158
 
143
- // CORS preflight
144
159
  if (req.method === "OPTIONS") {
145
160
  return new Response(null, { headers: corsHeaders });
146
161
  }
147
162
 
148
- // Token extraction: Authorization header > query param > cookie
149
- const authHeader = req.headers.get("Authorization");
150
- let rawToken =
151
- authHeader?.replace("Bearer ", "") ||
152
- url.searchParams.get("token");
153
-
154
- if (!rawToken) {
155
- const cookieHeader = req.headers.get("Cookie");
156
- if (cookieHeader) {
157
- const match = cookieHeader.match(/arc_token=([^;]+)/);
158
- if (match) rawToken = decodeURIComponent(match[1]);
163
+ // Inner request body extracted so we can wrap it in an HTTP span
164
+ // when telemetry is enabled. The wrap MUST set the active context
165
+ // (via runWithExtractedContext + startSpan) so nested handlers'
166
+ // spans attach to this one as parent.
167
+ const handleRequest = async (span: Span | undefined): Promise<Response> => {
168
+ const authHeader = req.headers.get("Authorization");
169
+ let rawToken =
170
+ authHeader?.replace("Bearer ", "") ||
171
+ url.searchParams.get("token");
172
+
173
+ if (!rawToken) {
174
+ const cookieHeader = req.headers.get("Cookie");
175
+ if (cookieHeader) {
176
+ const match = cookieHeader.match(/arc_token=([^;]+)/);
177
+ if (match) rawToken = decodeURIComponent(match[1]);
178
+ }
159
179
  }
160
- }
180
+ const tokenPayload = rawToken ? verifyToken(rawToken) : null;
161
181
 
162
- const tokenPayload = rawToken ? verifyToken(rawToken) : null;
163
-
164
- // WebSocket upgrade
165
- if (
166
- url.pathname === "/ws" &&
167
- req.headers.get("Upgrade") === "websocket"
168
- ) {
169
- if (server.upgrade(req, { data: { clientId: "" } })) return undefined;
170
- return new Response("WebSocket upgrade failed", {
171
- status: 500,
172
- headers: corsHeaders,
173
- });
174
- }
182
+ if (
183
+ url.pathname === "/ws" &&
184
+ req.headers.get("Upgrade") === "websocket"
185
+ ) {
186
+ if (server.upgrade(req, { data: { clientId: "" } })) return undefined as unknown as Response;
187
+ return new Response("WebSocket upgrade failed", {
188
+ status: 500,
189
+ headers: corsHeaders,
190
+ });
191
+ }
175
192
 
176
- // Run HTTP handler chain
177
- const reqCtx: ArcRequestContext = {
178
- rawToken,
179
- tokenPayload,
180
- corsHeaders,
193
+ const reqCtx: ArcRequestContext = { rawToken, tokenPayload, corsHeaders };
194
+ for (const handler of httpHandlers) {
195
+ const response = await handler(req, url, reqCtx);
196
+ if (response) {
197
+ span?.setAttribute("http.response.status_code", response.status);
198
+ return response;
199
+ }
200
+ }
201
+ span?.setAttribute("http.response.status_code", 404);
202
+ return new Response("Not Found", { status: 404, headers: corsHeaders });
181
203
  };
182
- for (const handler of httpHandlers) {
183
- const response = await handler(req, url, reqCtx);
184
- if (response) return response;
185
- }
186
204
 
187
- return new Response("Not Found", {
188
- status: 404,
189
- headers: corsHeaders,
190
- });
205
+ const telemetry = config.telemetry;
206
+ if (!telemetry) return handleRequest(undefined);
207
+
208
+ return telemetry.runWithExtractedContext(req.headers, () =>
209
+ telemetry.startSpan(
210
+ `${req.method} ${url.pathname}`,
211
+ (span) => handleRequest(span),
212
+ {
213
+ kind: 2 /* SpanKind.SERVER */,
214
+ attributes: {
215
+ "http.request.method": req.method,
216
+ "http.route": url.pathname,
217
+ "url.scheme": url.protocol.replace(":", ""),
218
+ "url.path": url.pathname,
219
+ "server.address": url.host,
220
+ "user_agent.original": req.headers.get("user-agent") ?? undefined,
221
+ },
222
+ },
223
+ ),
224
+ );
191
225
  },
192
226
 
193
227
  websocket: {
@@ -198,15 +232,53 @@ export async function createArcServer(
198
232
  const client = connectionManager.getClientByWs(ws as any);
199
233
  if (!client) return;
200
234
 
235
+ let message: any;
201
236
  try {
202
- const message = JSON.parse(messageStr as string);
203
- for (const handler of wsHandlers) {
204
- const handled = await handler(client, message, wsCtx);
205
- if (handled) break;
206
- }
237
+ message = JSON.parse(messageStr as string);
207
238
  } catch (error) {
208
239
  console.error("Failed to parse WS message:", error);
240
+ return;
209
241
  }
242
+
243
+ const dispatch = async (): Promise<void> => {
244
+ try {
245
+ for (const handler of wsHandlers) {
246
+ const handled = await handler(client, message, wsCtx);
247
+ if (handled) break;
248
+ }
249
+ } catch (error) {
250
+ console.error("WS handler error:", error);
251
+ }
252
+ };
253
+
254
+ const telemetry = config.telemetry;
255
+ if (!telemetry) {
256
+ await dispatch();
257
+ return;
258
+ }
259
+
260
+ // Parent context can travel inside the message under `traceparent`
261
+ // (and optionally `tracestate`) so distributed traces from the
262
+ // browser propagate over the persistent WS connection.
263
+ const carrier: Record<string, string> = {};
264
+ if (typeof message?.traceparent === "string") carrier.traceparent = message.traceparent;
265
+ if (typeof message?.tracestate === "string") carrier.tracestate = message.tracestate;
266
+
267
+ await telemetry.runWithExtractedContext(carrier, () =>
268
+ telemetry.startSpan(
269
+ `ws.${message?.type ?? "message"}`,
270
+ () => dispatch(),
271
+ {
272
+ kind: 2 /* SpanKind.SERVER */,
273
+ attributes: {
274
+ "messaging.system": "arc-ws",
275
+ "messaging.operation": "process",
276
+ "messaging.message.type": message?.type,
277
+ "arc.ws.client_id": client.id,
278
+ },
279
+ },
280
+ ),
281
+ );
210
282
  },
211
283
  close(ws) {
212
284
  const client = connectionManager.getClientByWs(ws as any);