@glasstrace/sdk 1.1.3 → 1.2.0

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.
package/README.md CHANGED
@@ -280,7 +280,7 @@ file directly and no longer needs the runtime handler.
280
280
 
281
281
  ## Subpath exports
282
282
 
283
- `@glasstrace/sdk` ships three public entries:
283
+ `@glasstrace/sdk` ships four public entries:
284
284
 
285
285
  - **`@glasstrace/sdk`** — primary import site. Use from
286
286
  `instrumentation.ts` (runtime instrumentation) and `next.config.ts`
@@ -298,6 +298,8 @@ file directly and no longer needs the runtime handler.
298
298
  condition; non-Node runtimes (workerd, edge-light) fail cleanly at
299
299
  module resolution rather than at evaluation.
300
300
  - **`@glasstrace/sdk/drizzle`** — Drizzle ORM adapter.
301
+ - **`@glasstrace/sdk/trpc`** — tRPC middleware-chain instrumentation.
302
+ See "tRPC middleware instrumentation" below.
301
303
 
302
304
  The source-map and import-graph helpers previously reachable from the
303
305
  `@glasstrace/sdk` root specifier have moved to `@glasstrace/sdk/node`
@@ -383,6 +385,57 @@ on the Node-only side to become edge-safe, the right move is to remove
383
385
  the `process` and Node built-in reaches from the symbol's transitive
384
386
  closure, not to add a runtime guard.
385
387
 
388
+ ## tRPC middleware instrumentation
389
+
390
+ The `@glasstrace/sdk/trpc` subpath exposes `tracedMiddleware`, a thin
391
+ wrapper that turns a user-supplied tRPC middleware function into a
392
+ span-emitting middleware function. Each invocation opens a child span
393
+ named `options.name` under the active OTel context (typically the HTTP
394
+ server span), so middleware steps land as children of the HTTP span
395
+ without manual context plumbing. Errors thrown from the middleware
396
+ body are recorded via `span.recordException` and propagate unchanged;
397
+ short-circuit `{ ok: false, error }` results mark the span `ERROR`
398
+ without recording an exception.
399
+
400
+ `@trpc/server` is declared as an optional peer dependency
401
+ (`^10.0.0 || ^11.0.0`); projects that do not use tRPC pay no runtime
402
+ cost because the subpath is excluded from the root barrel and is
403
+ tree-shakeable.
404
+
405
+ ```ts
406
+ // trpc.ts — your project
407
+ import { initTRPC, TRPCError } from "@trpc/server";
408
+ import { tracedMiddleware } from "@glasstrace/sdk/trpc";
409
+
410
+ interface MyContext { session?: { userId: string }; tier?: string }
411
+ const t = initTRPC.context<MyContext>().create();
412
+
413
+ const isAuthed = t.middleware(
414
+ tracedMiddleware({ name: "isAuthed" }, async ({ ctx, next }) => {
415
+ if (!ctx.session) throw new TRPCError({ code: "UNAUTHORIZED" });
416
+ return next({ ctx: { ...ctx, session: ctx.session } });
417
+ }),
418
+ );
419
+
420
+ const isPro = t.middleware(
421
+ tracedMiddleware({ name: "isPro" }, async ({ ctx, next }) => {
422
+ if (ctx.tier !== "pro") throw new TRPCError({ code: "FORBIDDEN" });
423
+ return next();
424
+ }),
425
+ );
426
+
427
+ export const proProcedure = t.procedure.use(isAuthed).use(isPro);
428
+ ```
429
+
430
+ The wrapped function preserves the original middleware's call-site type,
431
+ so tRPC's procedure-builder context narrowing flows through unchanged.
432
+ The existing `glasstrace.trpc.procedure` attribute (set on the parent
433
+ HTTP span) is not duplicated on the middleware child spans — middleware
434
+ spans carry only `trpc.path`, `trpc.type`, and any caller-supplied
435
+ `options.attributes`. Caller-supplied attributes are forwarded as-is;
436
+ the SDK does not redact them, so callers must avoid placing tokens or
437
+ credentials in `options.attributes`.
438
+
386
439
  ## Security
387
440
 
388
441
  The SDK transmits your API key exclusively via the `Authorization: Bearer`
@@ -3906,8 +3906,51 @@ function createDiscoveryHandler(getAnonKey, getSessionId, getClaimState) {
3906
3906
 
3907
3907
  // src/context-manager.ts
3908
3908
  import { AsyncLocalStorage } from "node:async_hooks";
3909
+ var GLASSTRACE_BRAND = 1;
3910
+ var GUARD = /* @__PURE__ */ Symbol.for("glasstrace.context-manager.installed");
3911
+ var OTEL_API_KEY = /* @__PURE__ */ Symbol.for("opentelemetry.js.api.1");
3912
+ function isOtelContextManager(value) {
3913
+ if (typeof value !== "object" || value === null) return false;
3914
+ const candidate = value;
3915
+ return typeof candidate.active === "function" && typeof candidate.with === "function" && typeof candidate.bind === "function" && typeof candidate.enable === "function" && typeof candidate.disable === "function";
3916
+ }
3917
+ function isInstallationRecord(value) {
3918
+ if (typeof value !== "object" || value === null) return false;
3919
+ const candidate = value;
3920
+ if (candidate.glasstraceContextManagerBrand !== GLASSTRACE_BRAND) return false;
3921
+ return candidate.manager === null || isOtelContextManager(candidate.manager);
3922
+ }
3923
+ function getOtelRegisteredContextManager() {
3924
+ const otelSlot = globalThis[OTEL_API_KEY];
3925
+ if (typeof otelSlot !== "object" || otelSlot === null) return void 0;
3926
+ const ctx = otelSlot.context;
3927
+ return isOtelContextManager(ctx) ? ctx : void 0;
3928
+ }
3909
3929
  function installContextManager() {
3910
3930
  try {
3931
+ const slot = globalThis;
3932
+ const existing = slot[GUARD];
3933
+ const otelCurrent = getOtelRegisteredContextManager();
3934
+ if (isInstallationRecord(existing) && existing.manager !== null && existing.manager === otelCurrent) {
3935
+ return true;
3936
+ }
3937
+ if (isInstallationRecord(existing) && existing.manager === null && otelCurrent !== void 0) {
3938
+ return false;
3939
+ }
3940
+ if (isInstallationRecord(existing) && existing.manager !== null) {
3941
+ const reSuccess = context.setGlobalContextManager(existing.manager);
3942
+ if (!reSuccess) {
3943
+ console.warn(
3944
+ "[glasstrace] Another context manager is already registered. Trace context propagation may not work as expected."
3945
+ );
3946
+ }
3947
+ const reRecord = {
3948
+ glasstraceContextManagerBrand: GLASSTRACE_BRAND,
3949
+ manager: reSuccess ? existing.manager : null
3950
+ };
3951
+ slot[GUARD] = reRecord;
3952
+ return reSuccess;
3953
+ }
3911
3954
  const als = new AsyncLocalStorage();
3912
3955
  const contextManager = {
3913
3956
  active: () => als.getStore() ?? ROOT_CONTEXT,
@@ -3928,6 +3971,11 @@ function installContextManager() {
3928
3971
  "[glasstrace] Another context manager is already registered. Trace context propagation may not work as expected."
3929
3972
  );
3930
3973
  }
3974
+ const record = {
3975
+ glasstraceContextManagerBrand: GLASSTRACE_BRAND,
3976
+ manager: success ? contextManager : null
3977
+ };
3978
+ slot[GUARD] = record;
3931
3979
  return success;
3932
3980
  } catch {
3933
3981
  return false;
@@ -4153,7 +4201,7 @@ function registerGlasstrace(options) {
4153
4201
  setCoreState(CoreState.REGISTERING);
4154
4202
  startRuntimeStateWriter({
4155
4203
  projectRoot: process.cwd(),
4156
- sdkVersion: "1.1.3"
4204
+ sdkVersion: "1.2.0"
4157
4205
  });
4158
4206
  const config = resolveConfig(options);
4159
4207
  if (config.verbose) {
@@ -4319,8 +4367,8 @@ async function backgroundInit(config, anonKeyForInit, generation) {
4319
4367
  if (config.verbose) {
4320
4368
  console.info("[glasstrace] Background init firing.");
4321
4369
  }
4322
- const healthReport = collectHealthReport("1.1.3");
4323
- const initResult = await performInit(config, anonKeyForInit, "1.1.3", healthReport);
4370
+ const healthReport = collectHealthReport("1.2.0");
4371
+ const initResult = await performInit(config, anonKeyForInit, "1.2.0", healthReport);
4324
4372
  if (generation !== registrationGeneration) return;
4325
4373
  const currentState = getCoreState();
4326
4374
  if (currentState === CoreState.SHUTTING_DOWN || currentState === CoreState.SHUTDOWN) {
@@ -4343,7 +4391,7 @@ async function backgroundInit(config, anonKeyForInit, generation) {
4343
4391
  }
4344
4392
  maybeInstallConsoleCapture();
4345
4393
  if (didLastInitSucceed()) {
4346
- startHeartbeat(config, anonKeyForInit, "1.1.3", generation, (newApiKey, accountId) => {
4394
+ startHeartbeat(config, anonKeyForInit, "1.2.0", generation, (newApiKey, accountId) => {
4347
4395
  setAuthState(AuthState.CLAIMING);
4348
4396
  emitLifecycleEvent("auth:claim_started", { accountId });
4349
4397
  setResolvedApiKey(newApiKey);
@@ -4692,4 +4740,4 @@ export {
4692
4740
  withGlasstraceConfig,
4693
4741
  captureError
4694
4742
  };
4695
- //# sourceMappingURL=chunk-FGDS33I2.js.map
4743
+ //# sourceMappingURL=chunk-6RIH6SFM.js.map