@glasstrace/sdk 1.1.2 → 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`
@@ -33,16 +33,17 @@ import {
33
33
  performInit,
34
34
  recordSpansDropped,
35
35
  recordSpansExported
36
- } from "./chunk-C567H5EQ.js";
36
+ } from "./chunk-JKI4OCFV.js";
37
37
  import {
38
38
  isAnonymousMode,
39
39
  isProductionDisabled,
40
40
  resolveConfig
41
41
  } from "./chunk-VUZCLMIX.js";
42
42
  import {
43
+ atomicWriteFileSync,
43
44
  getOrCreateAnonKey,
44
45
  readAnonKey
45
- } from "./chunk-3LILTM3T.js";
46
+ } from "./chunk-TWTWRJ25.js";
46
47
  import {
47
48
  GLASSTRACE_ATTRIBUTE_NAMES,
48
49
  deriveSessionId
@@ -3905,8 +3906,51 @@ function createDiscoveryHandler(getAnonKey, getSessionId, getClaimState) {
3905
3906
 
3906
3907
  // src/context-manager.ts
3907
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
+ }
3908
3929
  function installContextManager() {
3909
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
+ }
3910
3954
  const als = new AsyncLocalStorage();
3911
3955
  const contextManager = {
3912
3956
  active: () => als.getStore() ?? ROOT_CONTEXT,
@@ -3927,6 +3971,11 @@ function installContextManager() {
3927
3971
  "[glasstrace] Another context manager is already registered. Trace context propagation may not work as expected."
3928
3972
  );
3929
3973
  }
3974
+ const record = {
3975
+ glasstraceContextManagerBrand: GLASSTRACE_BRAND,
3976
+ manager: success ? contextManager : null
3977
+ };
3978
+ slot[GUARD] = record;
3930
3979
  return success;
3931
3980
  } catch {
3932
3981
  return false;
@@ -4058,7 +4107,7 @@ function registerHeartbeatShutdownHook(config, anonKey, sdkVersion) {
4058
4107
  }
4059
4108
 
4060
4109
  // src/runtime-state.ts
4061
- import { writeFileSync, renameSync, mkdirSync } from "node:fs";
4110
+ import { mkdirSync } from "node:fs";
4062
4111
  import { join } from "node:path";
4063
4112
  var _projectRoot = null;
4064
4113
  var _sdkVersion = "unknown";
@@ -4110,12 +4159,10 @@ function writeStateNow() {
4110
4159
  };
4111
4160
  const dir = join(_projectRoot, ".glasstrace");
4112
4161
  const filePath = join(dir, "runtime-state.json");
4113
- const tmpPath = join(dir, "runtime-state.json.tmp");
4114
4162
  mkdirSync(dir, { recursive: true, mode: 448 });
4115
- writeFileSync(tmpPath, JSON.stringify(runtimeState, null, 2) + "\n", {
4163
+ atomicWriteFileSync(filePath, JSON.stringify(runtimeState, null, 2) + "\n", {
4116
4164
  mode: 384
4117
4165
  });
4118
- renameSync(tmpPath, filePath);
4119
4166
  } catch (err) {
4120
4167
  sdkLog(
4121
4168
  "warn",
@@ -4154,7 +4201,7 @@ function registerGlasstrace(options) {
4154
4201
  setCoreState(CoreState.REGISTERING);
4155
4202
  startRuntimeStateWriter({
4156
4203
  projectRoot: process.cwd(),
4157
- sdkVersion: "1.1.2"
4204
+ sdkVersion: "1.2.0"
4158
4205
  });
4159
4206
  const config = resolveConfig(options);
4160
4207
  if (config.verbose) {
@@ -4320,8 +4367,8 @@ async function backgroundInit(config, anonKeyForInit, generation) {
4320
4367
  if (config.verbose) {
4321
4368
  console.info("[glasstrace] Background init firing.");
4322
4369
  }
4323
- const healthReport = collectHealthReport("1.1.2");
4324
- const initResult = await performInit(config, anonKeyForInit, "1.1.2", healthReport);
4370
+ const healthReport = collectHealthReport("1.2.0");
4371
+ const initResult = await performInit(config, anonKeyForInit, "1.2.0", healthReport);
4325
4372
  if (generation !== registrationGeneration) return;
4326
4373
  const currentState = getCoreState();
4327
4374
  if (currentState === CoreState.SHUTTING_DOWN || currentState === CoreState.SHUTDOWN) {
@@ -4344,7 +4391,7 @@ async function backgroundInit(config, anonKeyForInit, generation) {
4344
4391
  }
4345
4392
  maybeInstallConsoleCapture();
4346
4393
  if (didLastInitSucceed()) {
4347
- startHeartbeat(config, anonKeyForInit, "1.1.2", generation, (newApiKey, accountId) => {
4394
+ startHeartbeat(config, anonKeyForInit, "1.2.0", generation, (newApiKey, accountId) => {
4348
4395
  setAuthState(AuthState.CLAIMING);
4349
4396
  emitLifecycleEvent("auth:claim_started", { accountId });
4350
4397
  setResolvedApiKey(newApiKey);
@@ -4693,4 +4740,4 @@ export {
4693
4740
  withGlasstraceConfig,
4694
4741
  captureError
4695
4742
  };
4696
- //# sourceMappingURL=chunk-Z35HKVSO.js.map
4743
+ //# sourceMappingURL=chunk-6RIH6SFM.js.map