@arcote.tech/arc-host 0.7.4 → 0.7.6

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,EACV,YAAY,EAEb,MAAM,uBAAuB,CAAC;AAC/B,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,CA4PpB"}
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.4",
7
+ "version": "0.7.6",
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.4",
11
+ "@arcote.tech/arc-adapter-db-sqlite": "^0.7.6",
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.4"
27
+ "@arcote.tech/arc": "^0.7.6",
28
+ "@arcote.tech/arc-otel": "^0.7.6"
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,8 @@
1
1
  import type { ArcContextAny, DatabaseAdapter } from "@arcote.tech/arc";
2
+ import type {
3
+ ArcTelemetry,
4
+ Span,
5
+ } from "@arcote.tech/arc-otel";
2
6
  import type { Server } from "bun";
3
7
  import jwt from "jsonwebtoken";
4
8
  import { ConnectionManager } from "./connection-manager";
@@ -29,6 +33,11 @@ export interface ArcServerConfig {
29
33
  jwtSecret?: string;
30
34
  /** Extra callback when a WS client disconnects (e.g. to clean up platform state) */
31
35
  onWsClose?: (clientId: string) => void;
36
+ /** Optional OpenTelemetry instance. When provided, the HTTP fetch handler
37
+ * and WS message dispatch are wrapped in spans, with parent context
38
+ * extracted from `traceparent` (header / message field). All nested
39
+ * handler code automatically attaches to the active span. */
40
+ telemetry?: ArcTelemetry;
32
41
  }
33
42
 
34
43
  export interface ArcServer {
@@ -55,9 +64,23 @@ export async function createArcServer(
55
64
  "arc-host-secret-change-in-production";
56
65
  const port = config.port || 5005;
57
66
 
58
- // Init context handler
59
- const dbAdapter = config.dbAdapterFactory(config.context);
60
- const contextHandler = new ContextHandler(config.context, dbAdapter);
67
+ // Init context handler — telemetry (if any) flows through so executeCommand
68
+ // emits `command.<name>` spans parented to the active HTTP/WS span. The
69
+ // adapter is wrapped post-await so every transaction's find/set/remove/
70
+ // commit lands as a child span automatically.
71
+ const rawDbAdapter = config.dbAdapterFactory(config.context);
72
+ const dbAdapter = config.telemetry
73
+ ? rawDbAdapter.then(async (a) => {
74
+ const { wrapDbAdapter } = await import("@arcote.tech/arc-otel");
75
+ const dbSystem = process.env.DATABASE_URL ? "postgresql" : "sqlite";
76
+ return wrapDbAdapter(a, config.telemetry, dbSystem);
77
+ })
78
+ : rawDbAdapter;
79
+ const contextHandler = new ContextHandler(
80
+ config.context,
81
+ dbAdapter,
82
+ config.telemetry,
83
+ );
61
84
  await contextHandler.init();
62
85
 
63
86
  // Start cron scheduler
@@ -136,58 +159,74 @@ export async function createArcServer(
136
159
 
137
160
  async fetch(req, server) {
138
161
  const url = new URL(req.url);
139
-
140
- // Build CORS headers with request origin
141
162
  const corsHeaders = buildCorsHeaders(req);
142
163
 
143
- // CORS preflight
144
164
  if (req.method === "OPTIONS") {
145
165
  return new Response(null, { headers: corsHeaders });
146
166
  }
147
167
 
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]);
168
+ // Inner request body extracted so we can wrap it in an HTTP span
169
+ // when telemetry is enabled. The wrap MUST set the active context
170
+ // (via runWithExtractedContext + startSpan) so nested handlers'
171
+ // spans attach to this one as parent.
172
+ const handleRequest = async (span: Span | undefined): Promise<Response> => {
173
+ const authHeader = req.headers.get("Authorization");
174
+ let rawToken =
175
+ authHeader?.replace("Bearer ", "") ||
176
+ url.searchParams.get("token");
177
+
178
+ if (!rawToken) {
179
+ const cookieHeader = req.headers.get("Cookie");
180
+ if (cookieHeader) {
181
+ const match = cookieHeader.match(/arc_token=([^;]+)/);
182
+ if (match) rawToken = decodeURIComponent(match[1]);
183
+ }
159
184
  }
160
- }
185
+ const tokenPayload = rawToken ? verifyToken(rawToken) : null;
161
186
 
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
- }
187
+ if (
188
+ url.pathname === "/ws" &&
189
+ req.headers.get("Upgrade") === "websocket"
190
+ ) {
191
+ if (server.upgrade(req, { data: { clientId: "" } })) return undefined as unknown as Response;
192
+ return new Response("WebSocket upgrade failed", {
193
+ status: 500,
194
+ headers: corsHeaders,
195
+ });
196
+ }
175
197
 
176
- // Run HTTP handler chain
177
- const reqCtx: ArcRequestContext = {
178
- rawToken,
179
- tokenPayload,
180
- corsHeaders,
198
+ const reqCtx: ArcRequestContext = { rawToken, tokenPayload, corsHeaders };
199
+ for (const handler of httpHandlers) {
200
+ const response = await handler(req, url, reqCtx);
201
+ if (response) {
202
+ span?.setAttribute("http.response.status_code", response.status);
203
+ return response;
204
+ }
205
+ }
206
+ span?.setAttribute("http.response.status_code", 404);
207
+ return new Response("Not Found", { status: 404, headers: corsHeaders });
181
208
  };
182
- for (const handler of httpHandlers) {
183
- const response = await handler(req, url, reqCtx);
184
- if (response) return response;
185
- }
186
209
 
187
- return new Response("Not Found", {
188
- status: 404,
189
- headers: corsHeaders,
190
- });
210
+ const telemetry = config.telemetry;
211
+ if (!telemetry) return handleRequest(undefined);
212
+
213
+ return telemetry.runWithExtractedContext(req.headers, () =>
214
+ telemetry.startSpan(
215
+ `${req.method} ${url.pathname}`,
216
+ (span) => handleRequest(span),
217
+ {
218
+ kind: 2 /* SpanKind.SERVER */,
219
+ attributes: {
220
+ "http.request.method": req.method,
221
+ "http.route": url.pathname,
222
+ "url.scheme": url.protocol.replace(":", ""),
223
+ "url.path": url.pathname,
224
+ "server.address": url.host,
225
+ "user_agent.original": req.headers.get("user-agent") ?? undefined,
226
+ },
227
+ },
228
+ ),
229
+ );
191
230
  },
192
231
 
193
232
  websocket: {
@@ -198,15 +237,53 @@ export async function createArcServer(
198
237
  const client = connectionManager.getClientByWs(ws as any);
199
238
  if (!client) return;
200
239
 
240
+ let message: any;
201
241
  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
- }
242
+ message = JSON.parse(messageStr as string);
207
243
  } catch (error) {
208
244
  console.error("Failed to parse WS message:", error);
245
+ return;
209
246
  }
247
+
248
+ const dispatch = async (): Promise<void> => {
249
+ try {
250
+ for (const handler of wsHandlers) {
251
+ const handled = await handler(client, message, wsCtx);
252
+ if (handled) break;
253
+ }
254
+ } catch (error) {
255
+ console.error("WS handler error:", error);
256
+ }
257
+ };
258
+
259
+ const telemetry = config.telemetry;
260
+ if (!telemetry) {
261
+ await dispatch();
262
+ return;
263
+ }
264
+
265
+ // Parent context can travel inside the message under `traceparent`
266
+ // (and optionally `tracestate`) so distributed traces from the
267
+ // browser propagate over the persistent WS connection.
268
+ const carrier: Record<string, string> = {};
269
+ if (typeof message?.traceparent === "string") carrier.traceparent = message.traceparent;
270
+ if (typeof message?.tracestate === "string") carrier.tracestate = message.tracestate;
271
+
272
+ await telemetry.runWithExtractedContext(carrier, () =>
273
+ telemetry.startSpan(
274
+ `ws.${message?.type ?? "message"}`,
275
+ () => dispatch(),
276
+ {
277
+ kind: 2 /* SpanKind.SERVER */,
278
+ attributes: {
279
+ "messaging.system": "arc-ws",
280
+ "messaging.operation": "process",
281
+ "messaging.message.type": message?.type,
282
+ "arc.ws.client_id": client.id,
283
+ },
284
+ },
285
+ ),
286
+ );
210
287
  },
211
288
  close(ws) {
212
289
  const client = connectionManager.getClientByWs(ws as any);