Skip to content

Comments

feat: Implementing trace of graphql executions#3215

Open
ommeirelles wants to merge 9 commits intocanary-v2from
feat/otel-traces
Open

feat: Implementing trace of graphql executions#3215
ommeirelles wants to merge 9 commits intocanary-v2from
feat/otel-traces

Conversation

@ommeirelles
Copy link
Contributor

@ommeirelles ommeirelles commented Feb 19, 2026

image

Summary by CodeRabbit

  • New Features

    • Added observability/telemetry with tracing for GraphQL resolvers and context-level trace propagation
    • Introduced a diagnostics package to manage telemetry and trace clients
  • Chores

    • Added OpenTelemetry-related dependencies and improved build configuration for observability
    • Made server/context initialization asynchronous to integrate telemetry; updated tests accordingly

@ommeirelles ommeirelles requested a review from a team as a code owner February 19, 2026 15:33
@ommeirelles ommeirelles requested review from hellofanny and renatomaurovtex and removed request for a team February 19, 2026 15:33
@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Walkthrough

Adds a new diagnostics package and integrates OpenTelemetry tracing across the API and core: context now carries OTEL and an id, resolvers are wrapped with ResolverTrace, trace clients are initialized at runtime, and build/test code updated to await async context creation. (48 words)

Changes

Cohort / File(s) Summary
New Diagnostics package
packages/diagnostics/package.json, packages/diagnostics/src/index.ts, packages/diagnostics/src/start.ts, packages/diagnostics/src/config.json, packages/diagnostics/@types/global.d.ts, packages/diagnostics/src/globals.ts, packages/diagnostics/tsconfig.json, packages/diagnostics/vite.config.mts
Adds @faststore/diagnostics package: telemetry/trace client setup, global diagnostics store, config for sampling, types, and build config. Exposes getTelemetryClient / getTraceClient and CONVENTIONS/OTELAPI.
API – telemetry integration & wrapper
packages/api/package.json, packages/api/src/observability/telemetry.ts, packages/api/src/platforms/vtex/index.ts, packages/api/src/platforms/vtex/clients/fetch.ts, packages/api/local/server.mjs
Adds ResolverTrace wrapper, populates GraphqlContext with id and OTEL, makes context factory async, wraps native resolvers with tracing, and updates package deps. Minor import change for package.json JSON import.
Core – tracing plumbing
packages/core/package.json, packages/core/src/instrumentation.ts, packages/core/src/server/options.ts, packages/core/src/server/index.ts
Adds diagnostics dependency and runtime wiring: withTraceClient to initialize trace client into apiOptions, async getEnvelop that awaits trace-enabled context factory, and register() to initialize instrumentation server-side.
Typings & build config
packages/api/src/typings/index.ts, packages/api/vite.config.mts
Exposes a generic Resolver type; adds version? and OTEL? to Options; Vite config now externalizes builtins dynamically, includes devDependencies, and makes sourcemap conditional on mode.
Tests & consumers updated to async context
packages/api/test/integration/*, packages/api/test/unit/*, packages/api/test/integration/queries.test.ts, packages/api/test/integration/mutations.test.ts, packages/api/test/unit/platforms/vtex/clients/commerce.test.ts
Tests and local server adjusted to await async GraphqlVtexContextFactory — runners and context creation now async per-test.
Misc: core package change
packages/core/package.json
Adds @faststore/diagnostics dependency to core package.json.
UI styles whitespace
packages/ui/src/styles/base/utilities.scss
Whitespace/formatting change only.

Sequence Diagram

sequenceDiagram
    participant Client
    participant GraphQL as GraphQL Server
    participant ContextFactory as Context Factory
    participant ResolverTrace as ResolverTrace Wrapper
    participant Tracer as Trace Client (OTEL)
    participant Resolver as Original Resolver

    Client->>GraphQL: Send GraphQL request
    GraphQL->>ContextFactory: Build context (await)
    ContextFactory->>GraphQL: Return context with id + OTEL
    GraphQL->>ResolverTrace: Invoke wrapped resolver
    ResolverTrace->>Tracer: Extract/activate context, start span
    ResolverTrace->>Resolver: Execute original resolver
    Resolver-->>ResolverTrace: Return (sync or Promise)
    alt async result
        Resolver-->>ResolverTrace: Promise resolves
    end
    ResolverTrace->>Tracer: End span
    ResolverTrace-->>GraphQL: Return result
    GraphQL-->>Client: Respond
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • hellofanny
  • lemagnetic

Poem

A tiny id, an OTEL thread,
Spans bloom where resolvers tread.
From context built and traces spun,
Each request now tells where it's run. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically describes the main change: implementing OpenTelemetry tracing for GraphQL resolver executions across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/otel-traces

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codesandbox-ci
Copy link

codesandbox-ci bot commented Feb 19, 2026

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@ommeirelles ommeirelles changed the title Implementing trace of graphql executions feat: Implementing trace of graphql executions Feb 19, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/api/src/platforms/vtex/index.ts (1)

43-65: ⚠️ Potential issue | 🔴 Critical

Bug: id is generated once per factory, not per request — all requests share the same ID.

crypto.randomBytes(32) on line 49 executes in the outer factory scope. The inner closure (line 50) is what runs per-request, but it reuses the same id. This contradicts the "Request scope ID" comment on line 21. Move id generation inside the returned function:

Proposed fix
 export const GraphqlVtexContextFactory = (options: Options) => {
   getTelemetryClient({
     name,
     version,
     account: options.account,
   })
-  const id = crypto.randomBytes(32).toString('hex')
   return (ctx: any): GraphqlContext => {
-    ctx.id = id
+    ctx.id = crypto.randomBytes(32).toString('hex')
     ctx.storage = {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/platforms/vtex/index.ts` around lines 43 - 65, The factory
GraphqlVtexContextFactory currently generates id once in the outer scope (const
id = crypto.randomBytes(32).toString('hex')) so every request shares the same
id; move the id generation into the returned function so that inside the
returned (ctx: any): GraphqlContext => { ... } you create a fresh id (e.g.,
const id = crypto.randomBytes(32).toString('hex')) and then assign ctx.id = id,
removing the outer const id to ensure a unique request-scoped id for each ctx.
🧹 Nitpick comments (9)
packages/diagnostics/vite.config.mts (1)

21-23: dependencies ?? {} is a redundant null guard.

dependencies is already initialized with = {} on line 6, so it can never be undefined here.

♻️ Proposed fix
     external: [
       ...builtinModules.concat(builtinModules.map((e) => `node:${e}`)),
-      ...Object.keys({
-        ...(dependencies ?? {}),
-      }),
+      ...Object.keys(dependencies),
     ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/diagnostics/vite.config.mts` around lines 21 - 23, The spread uses a
redundant nullish fallback because the variable dependencies is initialized to
{} earlier; update the Object.keys spread to use dependencies directly (remove
the "?? {}" guard) so the line becomes Object.keys({ ...(dependencies) }) or
simply Object.keys(dependencies) as appropriate, ensuring you reference the
existing dependencies variable used on line 6 and the Object.keys(...)
expression to locate and modify the code.
packages/diagnostics/tsconfig.json (1)

4-4: outDir: "dist/esm" doesn't match Vite build output paths.

The Vite config outputs to dist/es/ and dist/cjs/, not dist/esm/. Since vite-plugin-dts drives the declaration output (not tsc), this is non-functional, but it's misleading for anyone running a bare tsc check on this package.

♻️ Suggested fix
-    "outDir": "dist/esm",
+    "outDir": "dist",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/diagnostics/tsconfig.json` at line 4, The tsconfig.json currently
sets "outDir": "dist/esm" which mismatches the Vite outputs and is misleading;
update the tsconfig.json's outDir key to "dist/es" to match Vite's ES output (or
remove the outDir entry entirely if you prefer to avoid any tsc emit assumptions
since vite-plugin-dts drives declaration output) so that the "outDir" setting
reflects the actual build paths used by the project.
packages/api/src/typings/index.ts (1)

32-37: info parameter should be GraphQLResolveInfo, not any.

info carries the full GraphQL resolution context (field name, parent type, path, schema, variables). Typing it as any opts out of type safety for every caller and forfeits valuable metadata for tracing. GraphQLResolveInfo is available through the existing graphql peer dependency.

♻️ Proposed fix
+import type { GraphQLResolveInfo } from 'graphql'

 type Resolver<
   TContext extends Record<string, any>,
   TSource = any,
   TVars = any,
   TReturn = any,
-> = (source: TSource, vars: TVars, context: TContext, info: any) => TReturn
+> = (source: TSource, vars: TVars, context: TContext, info: GraphQLResolveInfo) => TReturn

As per coding guidelines: "Type safety with generated types" and "Ensure type safety and avoid type assertions when possible."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/typings/index.ts` around lines 32 - 37, The Resolver type
currently types the fourth parameter as any; change it to GraphQLResolveInfo to
preserve GraphQL metadata and enable type safety: import { GraphQLResolveInfo }
from "graphql" at the top of the file and update the Resolver signature to
(source: TSource, vars: TVars, context: TContext, info: GraphQLResolveInfo) =>
TReturn (keeping the generics intact); ensure the graphql peer dependency is
used (no type assertions needed) so callers get proper info typing.
packages/diagnostics/package.json (1)

12-12: license: "ISC" is inconsistent with the rest of the monorepo.

Every other @faststore/* package uses "MIT". Consider aligning unless ISC is deliberate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/diagnostics/package.json` at line 12, The package's license field in
its package.json is set to "ISC" while the monorepo uses "MIT"; update the
"license" property from "ISC" to "MIT" in this package's package.json so it
matches other `@faststore` packages, then run your repo's license/validation
script (or npm install/test) to confirm no further inconsistencies.
packages/api/src/platforms/vtex/index.ts (2)

67-71: Remove commented-out code.

Dead code adds noise. If VtexResolver is needed later, it can be retrieved from git history.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/platforms/vtex/index.ts` around lines 67 - 71, Remove the
commented-out VtexResolver block: delete the entire commented code that defines
VtexResolver (the lines referencing VtexResolver, GraphqlResolver,
ResolverTrace, and GraphqlContext) so the file no longer contains this dead
code; if needed later it can be restored from git history.

80-96: finalResolvers: any drops type safety; variable name shadows module-level import.

Two concerns in getResolvers:

  1. Line 83: finalResolvers: any discards the resolver type structure. Use a more descriptive intermediate type or infer from nativeGetResolvers().
  2. Line 86: The destructured name in .map(([name, resolverFunction]) shadows the name import from package.json (line 5). Rename to e.g. resolverName for clarity.
Proposed fix
 export function getResolvers() {
   const resolvers = nativeGetResolvers()
 
-  const finalResolvers: any = {}
+  const finalResolvers: Record<string, Record<string, unknown>> = {}
   for (const [key, resolver] of Object.entries(resolvers)) {
     finalResolvers[key] = Object.fromEntries(
-      Object.entries(resolver).map(([name, resolverFunction]) => {
-        if (typeof resolverFunction !== 'function')
-          return [name, resolverFunction]
+      Object.entries(resolver).map(([resolverName, resolverFunction]) => {
+        if (typeof resolverFunction !== 'function')
+          return [resolverName, resolverFunction]
 
-        return [name, ResolverTrace(resolverFunction, `${key}(${name})`)]
+        return [resolverName, ResolverTrace(resolverFunction, `${key}(${resolverName})`)]
       })
     )
   }

As per coding guidelines, "Ensure type safety and avoid type assertions when possible".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/platforms/vtex/index.ts` around lines 80 - 96, getResolvers
currently weakens types by declaring finalResolvers: any and also shadows the
module-level import name via the map destructuring; update getResolvers to infer
types from nativeGetResolvers() (e.g., let resolvers = nativeGetResolvers(); use
const finalResolvers: Partial<typeof resolvers> = {} or an appropriate mapped
type) and replace the destructured variable name in
Object.entries(...).map(([name, resolverFunction]) => ...) with a
non-conflicting identifier like resolverName, then assign
ResolverTrace(resolverFunction, `${key}(${resolverName})`) so type safety is
preserved and the imported name is not shadowed.
packages/diagnostics/src/index.ts (2)

1-11: Namespace re-exports defeat tree-shaking — significant bundle size impact.

import * of @opentelemetry/api and @opentelemetry/semantic-conventions, then re-exporting as OTELAPI and CONVENTIONS, forces bundlers to include the entire modules. Downstream code only uses a handful of symbols (context, SpanKind, ATTR_CODE_FUNCTION_NAME, ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION).

Consider re-exporting only the specific symbols needed, or let consumers import directly from @opentelemetry/* packages.

As per coding guidelines, "Consider bundle size impact of dependencies".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/diagnostics/src/index.ts` around lines 1 - 11, The current namespace
re-exports (OTELAPI and CONVENTIONS) pull in entire `@opentelemetry` packages and
defeat tree-shaking; replace the wildcard imports and re-exports with named
exports for only the symbols actually used (e.g., export { context, SpanKind }
from '@opentelemetry/api' and export { ATTR_CODE_FUNCTION_NAME,
ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from
'@opentelemetry/semantic-conventions'), or remove the OTELAPI/CONVENTIONS
exports and let consumers import directly; update any internal references to
OTELAPI or CONVENTIONS to use the named symbols or the original package imports
to restore tree-shaking and reduce bundle size.

13-18: Redundant default export alongside named exports.

The default export duplicates everything already available via named exports. This creates two ways to import the same thing with no benefit, and makes tree-shaking harder.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/diagnostics/src/index.ts` around lines 13 - 18, Remove the redundant
default export object that mirrors the named exports; delete the default export
block that returns OTELAPI, CONVENTIONS, getTelemetryClient, and getTraceClient
and rely solely on the existing named exports. Ensure the named exports OTELAPI,
CONVENTIONS, getTelemetryClient, and getTraceClient are exported where they are
defined (or add explicit export statements) so consumers import them by name and
tree-shaking works correctly.
packages/api/src/observability/telemetry.ts (1)

7-14: Return type TReturn is inaccurate when wrapping async resolvers.

When fn returns Promise<X> (so TReturn = Promise<X>), the .then() on line 49 also produces Promise<X>, so the as TReturn cast on line 52 happens to be correct at runtime. However, the outer function's declared return type is TReturn, yet OTELAPI.context.with(activeContext, callback) returns whatever the callback returns — the TypeScript compiler can't verify the span-wrapping logic preserves the type. This is acceptable for now but worth noting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/observability/telemetry.ts` around lines 7 - 14, The
ResolverTrace wrapper's declared return type TReturn can diverge from the actual
return of the wrapped function (especially when the wrapped function returns a
Promise); update the ResolverTrace signature to take the resolver function as a
generic function type (e.g., F extends (source: TSource, vars: TVars, context:
TContext, info: any) => any) and use ReturnType<F> as the wrapper's return type
so the compiler preserves the exact return (Promise or non-Promise) of the
original fn; locate the ResolverTrace declaration and replace the TReturn
generic with an F generic and use ReturnType<F> for the outer function's return
type and for internal casts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/api/package.json`:
- Around line 77-78: Remove the unnecessary explicit peerDependency entry for
"@vtex/diagnostics-nodejs" from the package.json for the `@faststore/api` package:
delete the "@vtex/diagnostics-nodejs": "^5.1.1" line from peerDependencies
(since `@faststore/diagnostics` already depends on it and the code in
`@faststore/api` does not import it), then update lockfiles/install
(npm/yarn/pnpm) as needed to ensure the dependency is resolved transitively and
run tests/build to confirm nothing breaks.

In `@packages/api/src/observability/telemetry.ts`:
- Around line 40-52: The span is not ended when fn throws synchronously or when
the returned promise rejects; wrap the invocation of fn (the call that assigns
returnedValue inside the context.with callback) in a try/catch so that
span.end() is called in the catch before rethrowing synchronous errors, and for
the async case replace the current then-only logic with a finally (or attach
both then and catch) on the Promise (e.g.,
Promise.resolve(returnedValue).finally(...)) to ensure span.end() runs on
resolve or rejection; reference the variables/functions returnedValue, fn, span
and the context.with callback when making the changes.

In `@packages/api/src/platforms/vtex/index.ts`:
- Around line 44-48: getTelemetryClient is called without awaiting so the trace
client (TRACE_CLIENT) isn't initialized before withTraceClient runs; make
GraphqlVtexContextFactory async and await getTelemetryClient(...) inside it so
the telemetry client is ready when GraphqlVtexContextFactory returns, and then
update the caller that invokes this factory (the place where withTraceClient is
composed with the factory) to await the async GraphqlVtexContextFactory so OTEL
context is populated for early requests.

In `@packages/core/package.json`:
- Line 66: Remove the direct dependency entry for "@vtex/diagnostics-nodejs"
from the dependencies list in package.json (the explicit line that reads
"@vtex/diagnostics-nodejs": "^5.1.1") because it is already provided
transitively by "@faststore/diagnostics"; update the dependencies section to
delete that line so only "@faststore/diagnostics" remains responsible for
bringing in that package.

In `@packages/core/src/instrumentation.ts`:
- Around line 1-11: The register function can throw from its dynamic imports
(package.json, discovery.config, or `@faststore/diagnostics`) and must not crash
Next.js on startup; update the async function register to check
process.env.NEXT_RUNTIME as before but wrap the entire import/initialization
block in a try/catch, gracefully short-circuit on error (do not rethrow), and
emit a safe error message (e.g., via console.error or a fallback logger) so
telemetry failures do not prevent the app from starting; reference the register
function and the three dynamic imports (import('../package.json'),
import('../discovery.config'), import('@faststore/diagnostics')) when making the
change.

In `@packages/core/src/server/options.ts`:
- Around line 26-36: Change the generic and return typing and fix the
import/variable typo: make withTraceClient generic constrained to objects (e.g.
function withTraceClient<T extends object>(apiOptions: T): Promise<T & { OTEL:
unknown }>) so spreading ...apiOptions is type-safe and the return reflects the
added OTEL property; remove the useless optional chain on the dynamic import
(use await import('@faststore/diagnostics') instead of (await
import(...))?.getTraceClient()) so import errors propagate, rename traceCLient
to traceClient, call const traceClient = (await import(...)).getTraceClient();
and keep a defensive traceClient?.inject(OTEL) call for when getTraceClient()
returns undefined.

In `@packages/diagnostics/package.json`:
- Around line 1-33: The package.json for `@faststore/diagnostics` is missing a
publishConfig entry which causes npm to publish the scoped package as
restricted; update packages/diagnostics/package.json (package name
"@faststore/diagnostics") to include "publishConfig": { "access": "public" } at
the top level of the JSON so it mirrors other `@faststore/`* packages and can be
published successfully.

In `@packages/diagnostics/src/start.ts`:
- Around line 22-25: The traces exporter is forcing TLS off by hardcoding
insecure: true in the Exporters.CreateTracesExporterConfig call (tracesConfig) —
change this to derive from an environment variable instead (for example check
process.env.NODE_ENV !== 'production' or a dedicated OTLP_INSECURE flag) and
pass that boolean into the insecure option when calling
Exporters.CreateTracesExporterConfig( { endpoint: OTLP_ENDPOINT, insecure:
<derivedBool> } ) so production defaults to secure/TLS while non-production can
disable it via env.
- Line 60: The console.log call that prints "TELEMETRY CLIENT STARTED" with the
opt object should be removed or converted to structured debug logging: replace
the console.log('TELEMETRY CLIENT STARTED', opt) usage with a debug-level logger
(e.g., processLogger.debug or the project's logger) and ensure you do not emit
sensitive fields from the opt object—either omit or redact opt.account before
logging (or only log a non-identifying subset). Locate the console.log in
start.ts where the opt variable is used and change it to a debug-level,
structured log that excludes or masks the account identifier.
- Around line 32-63: getTelemetryClient currently recreates and overwrites the
global TELEMETRY_CLIENT and TRACE_CLIENT on every call; add an idempotency guard
at the start of getTelemetryClient that returns the existing TELEMETRY_CLIENT
immediately if it's already initialized (so you don't call NewTelemetryClient,
setupTracesExporter, newLogsClient/newMetricsClient/newTracesClient or
registerInstrumentations again). Locate getTelemetryClient and reference the
TELEMETRY_CLIENT and TRACE_CLIENT globals; implement an early return using the
same function signature and ensure you do not re-run setupTracesExporter,
NewTelemetryClient, or Instrumentation.CommonInstrumentations.minimal() when
TELEMETRY_CLIENT is present.
- Around line 51-52: The code incorrectly reuses tracesExporter (from
Exporters.CreateTracesExporterConfig) for logs and metrics; replace those calls
to create and pass signal-specific exporters by creating a logsExporter via
Exporters.CreateLogsExporterConfig() and a metricsExporter via
Exporters.CreateMetricsExporterConfig(), then call
TELEMETRY_CLIENT.newLogsClient({ exporter: logsExporter }) and
TELEMETRY_CLIENT.newMetricsClient({ exporter: metricsExporter }) while leaving
the traces client using tracesExporter.

In `@packages/diagnostics/vite.config.mts`:
- Around line 15-17: The Vite library config currently uses fileName:
'[format]/index' which makes Vite emit index.cjs for the 'cjs' format while
package.json's "require" expects ./dist/cjs/index.js; update the Vite config's
fileName to a function that returns a path forcing .js for the 'cjs' format and
.mjs for the ES format (i.e., return 'cjs/index.js' for format === 'cjs' and
'es/index.mjs' otherwise) so the emitted files match package.json, or
alternatively update package.json's "require" to point to ./dist/cjs/index.cjs
if you prefer Vite's default naming.

---

Outside diff comments:
In `@packages/api/src/platforms/vtex/index.ts`:
- Around line 43-65: The factory GraphqlVtexContextFactory currently generates
id once in the outer scope (const id = crypto.randomBytes(32).toString('hex'))
so every request shares the same id; move the id generation into the returned
function so that inside the returned (ctx: any): GraphqlContext => { ... } you
create a fresh id (e.g., const id = crypto.randomBytes(32).toString('hex')) and
then assign ctx.id = id, removing the outer const id to ensure a unique
request-scoped id for each ctx.

---

Nitpick comments:
In `@packages/api/src/observability/telemetry.ts`:
- Around line 7-14: The ResolverTrace wrapper's declared return type TReturn can
diverge from the actual return of the wrapped function (especially when the
wrapped function returns a Promise); update the ResolverTrace signature to take
the resolver function as a generic function type (e.g., F extends (source:
TSource, vars: TVars, context: TContext, info: any) => any) and use
ReturnType<F> as the wrapper's return type so the compiler preserves the exact
return (Promise or non-Promise) of the original fn; locate the ResolverTrace
declaration and replace the TReturn generic with an F generic and use
ReturnType<F> for the outer function's return type and for internal casts.

In `@packages/api/src/platforms/vtex/index.ts`:
- Around line 67-71: Remove the commented-out VtexResolver block: delete the
entire commented code that defines VtexResolver (the lines referencing
VtexResolver, GraphqlResolver, ResolverTrace, and GraphqlContext) so the file no
longer contains this dead code; if needed later it can be restored from git
history.
- Around line 80-96: getResolvers currently weakens types by declaring
finalResolvers: any and also shadows the module-level import name via the map
destructuring; update getResolvers to infer types from nativeGetResolvers()
(e.g., let resolvers = nativeGetResolvers(); use const finalResolvers:
Partial<typeof resolvers> = {} or an appropriate mapped type) and replace the
destructured variable name in Object.entries(...).map(([name, resolverFunction])
=> ...) with a non-conflicting identifier like resolverName, then assign
ResolverTrace(resolverFunction, `${key}(${resolverName})`) so type safety is
preserved and the imported name is not shadowed.

In `@packages/api/src/typings/index.ts`:
- Around line 32-37: The Resolver type currently types the fourth parameter as
any; change it to GraphQLResolveInfo to preserve GraphQL metadata and enable
type safety: import { GraphQLResolveInfo } from "graphql" at the top of the file
and update the Resolver signature to (source: TSource, vars: TVars, context:
TContext, info: GraphQLResolveInfo) => TReturn (keeping the generics intact);
ensure the graphql peer dependency is used (no type assertions needed) so
callers get proper info typing.

In `@packages/diagnostics/package.json`:
- Line 12: The package's license field in its package.json is set to "ISC" while
the monorepo uses "MIT"; update the "license" property from "ISC" to "MIT" in
this package's package.json so it matches other `@faststore` packages, then run
your repo's license/validation script (or npm install/test) to confirm no
further inconsistencies.

In `@packages/diagnostics/src/index.ts`:
- Around line 1-11: The current namespace re-exports (OTELAPI and CONVENTIONS)
pull in entire `@opentelemetry` packages and defeat tree-shaking; replace the
wildcard imports and re-exports with named exports for only the symbols actually
used (e.g., export { context, SpanKind } from '@opentelemetry/api' and export {
ATTR_CODE_FUNCTION_NAME, ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from
'@opentelemetry/semantic-conventions'), or remove the OTELAPI/CONVENTIONS
exports and let consumers import directly; update any internal references to
OTELAPI or CONVENTIONS to use the named symbols or the original package imports
to restore tree-shaking and reduce bundle size.
- Around line 13-18: Remove the redundant default export object that mirrors the
named exports; delete the default export block that returns OTELAPI,
CONVENTIONS, getTelemetryClient, and getTraceClient and rely solely on the
existing named exports. Ensure the named exports OTELAPI, CONVENTIONS,
getTelemetryClient, and getTraceClient are exported where they are defined (or
add explicit export statements) so consumers import them by name and
tree-shaking works correctly.

In `@packages/diagnostics/tsconfig.json`:
- Line 4: The tsconfig.json currently sets "outDir": "dist/esm" which mismatches
the Vite outputs and is misleading; update the tsconfig.json's outDir key to
"dist/es" to match Vite's ES output (or remove the outDir entry entirely if you
prefer to avoid any tsc emit assumptions since vite-plugin-dts drives
declaration output) so that the "outDir" setting reflects the actual build paths
used by the project.

In `@packages/diagnostics/vite.config.mts`:
- Around line 21-23: The spread uses a redundant nullish fallback because the
variable dependencies is initialized to {} earlier; update the Object.keys
spread to use dependencies directly (remove the "?? {}" guard) so the line
becomes Object.keys({ ...(dependencies) }) or simply Object.keys(dependencies)
as appropriate, ensuring you reference the existing dependencies variable used
on line 6 and the Object.keys(...) expression to locate and modify the code.

Comment on lines 40 to 52
const returnedValue = fn(source, vars, graphqlContext, info)

if (!span) return returnedValue

if (!(returnedValue instanceof Promise)) {
span.end()
return returnedValue
}

return returnedValue.then((value) => {
span.end()
return value
}) as TReturn
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Span is never ended on errors — both sync throws and promise rejections leak spans.

If fn() throws synchronously (line 40), execution exits the context.with callback without calling span.end(). Similarly, if the returned promise rejects, there's no .catch() to end the span. Leaked spans corrupt trace data and can cause memory pressure in the exporter.

Proposed fix
-      const returnedValue = fn(source, vars, graphqlContext, info)
+      let returnedValue: TReturn
+      try {
+        returnedValue = fn(source, vars, graphqlContext, info)
+      } catch (err) {
+        span?.setStatus({ code: OTELAPI.SpanStatusCode.ERROR })
+        span?.end()
+        throw err
+      }
 
       if (!span) return returnedValue
 
       if (!(returnedValue instanceof Promise)) {
         span.end()
         return returnedValue
       }
 
-      return returnedValue.then((value) => {
-        span.end()
-        return value
-      }) as TReturn
+      return returnedValue
+        .then((value) => {
+          span.end()
+          return value
+        })
+        .catch((err) => {
+          span.setStatus({ code: OTELAPI.SpanStatusCode.ERROR })
+          span.end()
+          throw err
+        }) as TReturn

As per coding guidelines, "Proper error handling and validation".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const returnedValue = fn(source, vars, graphqlContext, info)
if (!span) return returnedValue
if (!(returnedValue instanceof Promise)) {
span.end()
return returnedValue
}
return returnedValue.then((value) => {
span.end()
return value
}) as TReturn
let returnedValue: TReturn
try {
returnedValue = fn(source, vars, graphqlContext, info)
} catch (err) {
span?.setStatus({ code: OTELAPI.SpanStatusCode.ERROR })
span?.end()
throw err
}
if (!span) return returnedValue
if (!(returnedValue instanceof Promise)) {
span.end()
return returnedValue
}
return returnedValue
.then((value) => {
span.end()
return value
})
.catch((err) => {
span.setStatus({ code: OTELAPI.SpanStatusCode.ERROR })
span.end()
throw err
}) as TReturn
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/observability/telemetry.ts` around lines 40 - 52, The span
is not ended when fn throws synchronously or when the returned promise rejects;
wrap the invocation of fn (the call that assigns returnedValue inside the
context.with callback) in a try/catch so that span.end() is called in the catch
before rethrowing synchronous errors, and for the async case replace the current
then-only logic with a finally (or attach both then and catch) on the Promise
(e.g., Promise.resolve(returnedValue).finally(...)) to ensure span.end() runs on
resolve or rejection; reference the variables/functions returnedValue, fn, span
and the context.with callback when making the changes.

Comment on lines 1 to 11
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { name, version } = await import('../package.json')
const { api } = await import('../discovery.config')
;(await import('@faststore/diagnostics')).getTelemetryClient({
name,
version,
account: api.storeId,
})
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unhandled import failures can crash Next.js on startup.

Any of the three dynamic imports — package.json, discovery.config, or @faststore/diagnostics — can throw (e.g., missing discovery.config, misconfigured environment). Next.js doesn't catch errors thrown from register(), so a failed telemetry init will prevent the entire app from starting. Wrap the body in a try/catch and degrade gracefully.

🛡️ Proposed fix
 export async function register() {
   if (process.env.NEXT_RUNTIME === 'nodejs') {
-    const { name, version } = await import('../package.json')
-    const { api } = await import('../discovery.config')
-    ;(await import('@faststore/diagnostics')).getTelemetryClient({
-      name,
-      version,
-      account: api.storeId,
-    })
+    try {
+      const { name, version } = await import('../package.json')
+      const { api } = await import('../discovery.config')
+      ;(await import('@faststore/diagnostics')).getTelemetryClient({
+        name,
+        version,
+        account: api.storeId,
+      })
+    } catch (err) {
+      console.warn('[faststore] Telemetry initialization failed:', err)
+    }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/instrumentation.ts` around lines 1 - 11, The register
function can throw from its dynamic imports (package.json, discovery.config, or
`@faststore/diagnostics`) and must not crash Next.js on startup; update the async
function register to check process.env.NEXT_RUNTIME as before but wrap the
entire import/initialization block in a try/catch, gracefully short-circuit on
error (do not rethrow), and emit a safe error message (e.g., via console.error
or a fallback logger) so telemetry failures do not prevent the app from
starting; reference the register function and the three dynamic imports
(import('../package.json'), import('../discovery.config'),
import('@faststore/diagnostics')) when making the change.

Comment on lines 51 to 52
await TELEMETRY_CLIENT.newLogsClient({ exporter: tracesExporter })
await TELEMETRY_CLIENT.newMetricsClient({ exporter: tracesExporter })
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
find . -path "*/packages/diagnostics/src/start.ts" -type f

Repository: vtex/faststore

Length of output: 93


🏁 Script executed:

# Check the repository structure
ls -la packages/ 2>/dev/null | head -20
fd "start.ts" packages/diagnostics/

Repository: vtex/faststore

Length of output: 707


🏁 Script executed:

# Read the start.ts file to understand the context
cat -n packages/diagnostics/src/start.ts | head -80

Repository: vtex/faststore

Length of output: 2450


🏁 Script executed:

# Search for tracesExporter definition and Exporters.CreateTracesExporterConfig
rg "tracesExporter|CreateTracesExporterConfig" packages/diagnostics/src/ -A 2 -B 2

Repository: vtex/faststore

Length of output: 1364


🏁 Script executed:

# Search for newLogsClient and newMetricsClient definitions
rg "newLogsClient|newMetricsClient" packages/diagnostics/ -B 2 -A 5

Repository: vtex/faststore

Length of output: 700


🏁 Script executed:

# Search for other exporter creation methods
rg "CreateExporterConfig|CreateLogsExporter|CreateMetricsExporter" packages/ -t ts -t js

Repository: vtex/faststore

Length of output: 40


🏁 Script executed:

# Look for any package.json to understand dependencies
cat packages/diagnostics/package.json | grep -A 5 -B 5 "@vtex/diagnostics"

Repository: vtex/faststore

Length of output: 356


🏁 Script executed:

# Search for other usages of newLogsClient or newMetricsClient
rg "newLogsClient|newMetricsClient" . -t ts -t js --max-count=20

Repository: vtex/faststore

Length of output: 270


🏁 Script executed:

# Look for any patterns showing how exporters should be configured
rg "exporter.*config|exporter.*=" packages/ -t ts -B 2 -A 2

Repository: vtex/faststore

Length of output: 40


🌐 Web query:

@vtex/diagnostics-nodejs newLogsClient newMetricsClient exporter OTLP configuration

💡 Result:

1) Configure OTLP via the library config (recommended)

Create a telemetry.json (or whatever you pass in NewTelemetryClient(..., { config: { configPath } })) and set exporters per signal:

{
  "logs": {
    "exporters": {
      "mode": "otlp",
      "otlp": { "endpoint": "otel-collector:4317" }
    }
  },
  "metrics": {
    "exporters": {
      "mode": "otlp",
      "otlp": { "endpoint": "otel-collector:4317" }
    }
  }
}

The config schema supports mode: "otlp" | "console" | "none" and otlp.endpoint (plus optional headers, timeoutMs, batching/queue knobs, etc.). [1]

Then you can just do:

const telemetry = await NewTelemetryClient(appId, clientName, serviceName, { config: { configPath: './telemetry.json' } })
const logs = await telemetry.newLogsClient()
const metrics = await telemetry.newMetricsClient()

newLogsClient() / newMetricsClient() accept per-client override objects. [2]


2) Configure OTLP exporter programmatically and pass it to the clients

Build an OTLP exporter using the helpers and inject it via overrides (both logs + metrics configs accept exporter?: Exporter). [3][4][5]

import { NewTelemetryClient, Exporters } from '@vtex/diagnostics-nodejs'

const telemetry = await NewTelemetryClient(appId, clientName, serviceName)

const logsExporter = Exporters.CreateExporter(
  Exporters.CreateLogsExporterConfig({
    endpoint: 'otel-collector:4317',
    compression: 'gzip',
    timeoutSeconds: 30,
    insecure: true
  }),
  'otlp'
)

const metricsExporter = Exporters.CreateExporter(
  Exporters.CreateMetricsExporterConfig({
    endpoint: 'otel-collector:4317',
    compression: 'gzip',
    timeoutSeconds: 30,
    insecure: true
  }),
  'otlp'
)

const logs = await telemetry.newLogsClient({ exporter: logsExporter })
const metrics = await telemetry.newMetricsClient({ exporter: metricsExporter })

The exporter options include endpoint plus optional interval, timeoutSeconds, compression ('none'|'gzip'), and insecure. [3][6]


Sources
[1] https://cdn.jsdelivr.net/npm/@vtex/diagnostics-nodejs@5.1.1/dist/types/config.d.ts
[2] https://cdn.jsdelivr.net/npm/@vtex/diagnostics-nodejs@5.1.1/dist/telemetry/client.d.ts
[3] https://cdn.jsdelivr.net/npm/@vtex/diagnostics-nodejs@5.1.1/dist/types/exporters.d.ts
[4] https://cdn.jsdelivr.net/npm/@vtex/diagnostics-nodejs@5.1.1/dist/types/logs.d.ts
[5] https://cdn.jsdelivr.net/npm/@vtex/diagnostics-nodejs@5.1.1/dist/types/metrics.d.ts
[6] https://cdn.jsdelivr.net/npm/@vtex/diagnostics-nodejs@5.1.1/dist/exporters/index.d.ts


🏁 Script executed:

# Look for node_modules or any type definitions
find . -name "diagnostics-nodejs" -o -name "*diagnostics*" | grep -E "(node_modules|\.d\.ts)" | head -20

Repository: vtex/faststore

Length of output: 40


🌐 Web query:

@vtex/diagnostics-nodejs npm package documentation

💡 Result:

@vtex/diagnostics-nodejs documentation (npm)

What it is: An OpenTelemetry-based Node.js library that provides a unified setup for traces, metrics, and logs, plus auto-instrumentation (Express/Fastify/Koa/NestJS), rule-based sampling, and hot-reloaded remote config (local file or S3). It’s marked as VTEX proprietary / internal-use in the README. [1]

Install

npm install `@vtex/diagnostics-nodejs`

[1]

Minimal setup (important ordering)

Initialize the telemetry client before importing your framework, then create clients and register instrumentations. [1]

import { NewTelemetryClient, Instrumentation } from '@vtex/diagnostics-nodejs'

async function main() {
  const instrumentations = Instrumentation.CommonInstrumentations.express()

  const telemetryClient = await NewTelemetryClient(
    'my-app-id',
    'my-client-instance',
    'my-service-name'
  )

  const tracesClient = await telemetryClient.newTracesClient({ setGlobalProvider: true })
  const metricsClient = await telemetryClient.newMetricsClient()
  const logsClient = await telemetryClient.newLogsClient()

  telemetryClient.registerInstrumentations(instrumentations)

  const express = require('express')
  const app = express()

  app.get('/api/users', (req, res) => {
    logsClient.info('Users endpoint called', { userId: req.query.id })
    res.json({ users: [] })
  })

  app.listen(3000)
}

main().catch(console.error)

[1]

Remote config (hot reload)

File-based:

await NewTelemetryClient('app', 'client', 'service', {
  config: { configPath: './config/telemetry.json', enableHotReload: true, pollIntervalMs: 15000 }
})

S3-based:

await NewTelemetryClient('app', 'client', 'service', {
  config: {
    s3Config: { bucket: 'my-telemetry-configs', key: 'environments/prod/my-service.json', region: 'us-east-1' },
    enableHotReload: true,
    pollIntervalMs: 30000
  }
})

[1]

Primary doc/source: npm package README. [1]


Create separate exporters for logs, metrics, and traces signals.

The code reuses tracesExporter (created via Exporters.CreateTracesExporterConfig) for newLogsClient and newMetricsClient, but the library requires signal-specific exporter configs. Use Exporters.CreateLogsExporterConfig() for logs and Exporters.CreateMetricsExporterConfig() for metrics to avoid malformed telemetry data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/diagnostics/src/start.ts` around lines 51 - 52, The code incorrectly
reuses tracesExporter (from Exporters.CreateTracesExporterConfig) for logs and
metrics; replace those calls to create and pass signal-specific exporters by
creating a logsExporter via Exporters.CreateLogsExporterConfig() and a
metricsExporter via Exporters.CreateMetricsExporterConfig(), then call
TELEMETRY_CLIENT.newLogsClient({ exporter: logsExporter }) and
TELEMETRY_CLIENT.newMetricsClient({ exporter: metricsExporter }) while leaving
the traces client using tracesExporter.

TELEMETRY_CLIENT.registerInstrumentations(
Instrumentation.CommonInstrumentations.minimal()
)
console.log('TELEMETRY CLIENT STARTED', opt)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove console.log or downgrade to structured debug logging.

This logs the opt object which includes the account identifier in production. Use a proper logger at debug level, or remove entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/diagnostics/src/start.ts` at line 60, The console.log call that
prints "TELEMETRY CLIENT STARTED" with the opt object should be removed or
converted to structured debug logging: replace the console.log('TELEMETRY CLIENT
STARTED', opt) usage with a debug-level logger (e.g., processLogger.debug or the
project's logger) and ensure you do not emit sensitive fields from the opt
object—either omit or redact opt.account before logging (or only log a
non-identifying subset). Locate the console.log in start.ts where the opt
variable is used and change it to a debug-level, structured log that excludes or
masks the account identifier.

Comment on lines 15 to 17
formats: ['cjs', 'es'],
fileName: '[format]/index',
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check Vite version in the workspace
cat packages/diagnostics/package.json | grep -A2 '"vite"' || echo "vite not in package.json"

Repository: vtex/faststore

Length of output: 119


🏁 Script executed:

# Check pnpm-workspace catalog for vite version
cat pnpm-workspace.yaml 2>/dev/null | grep -A50 "catalog:" | grep -E "vite|^\s+[a-z]" | head -20

Repository: vtex/faststore

Length of output: 396


🏁 Script executed:

# Read the full vite.config.mts file
cat -n packages/diagnostics/vite.config.mts

Repository: vtex/faststore

Length of output: 909


🏁 Script executed:

# Check package.json exports/require fields
cat packages/diagnostics/package.json | grep -A10 '"exports"' || cat packages/diagnostics/package.json | grep -A5 '"require"'

Repository: vtex/faststore

Length of output: 344


🏁 Script executed:

# Check if dist folder exists and what files are generated
ls -la packages/diagnostics/dist/cjs/ 2>/dev/null || echo "dist/cjs not present"

Repository: vtex/faststore

Length of output: 78


🏁 Script executed:

# Search for any fileName function or custom extension handling in the codebase
rg -n "fileName" packages/diagnostics/ -A2 -B2

Repository: vtex/faststore

Length of output: 380


CJS output extension mismatch with package.json "require" field.

Vite 7 library mode with fileName: '[format]/index' emits .cjs for CJS format by default, but package.json declares "require": "./dist/cjs/index.js". CommonJS consumers will get a MODULE_NOT_FOUND error. Use a fileName function to force the .js extension for CJS and .mjs for ES:

fileName: (format) => `${format === 'cjs' ? 'cjs' : 'es'}/index.${format === 'cjs' ? 'js' : 'mjs'}`

Alternatively, update package.json to expect "require": "./dist/cjs/index.cjs" if you prefer Vite's default naming.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/diagnostics/vite.config.mts` around lines 15 - 17, The Vite library
config currently uses fileName: '[format]/index' which makes Vite emit index.cjs
for the 'cjs' format while package.json's "require" expects ./dist/cjs/index.js;
update the Vite config's fileName to a function that returns a path forcing .js
for the 'cjs' format and .mjs for the ES format (i.e., return 'cjs/index.js' for
format === 'cjs' and 'es/index.mjs' otherwise) so the emitted files match
package.json, or alternatively update package.json's "require" to point to
./dist/cjs/index.cjs if you prefer Vite's default naming.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/api/src/platforms/vtex/index.ts (1)

43-65: ⚠️ Potential issue | 🔴 Critical

id is generated once at factory creation — all requests share the same ID.

crypto.randomBytes(32) on line 49 runs once when GraphqlVtexContextFactory is awaited during server init. The returned closure reuses this single id for every incoming request. If id is meant to be a per-request correlation identifier (as the JSDoc "Request scope ID" on line 21 suggests), it needs to move inside the returned function.

Proposed fix
 export const GraphqlVtexContextFactory = async (options: Options) => {
   await getTelemetryClient({
     name,
     version,
     account: options.account,
   })
-  const id = crypto.randomBytes(32).toString('hex')
   return (ctx: any): GraphqlContext => {
-    ctx.id = id
+    ctx.id = crypto.randomBytes(32).toString('hex')
     ctx.storage = {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/platforms/vtex/index.ts` around lines 43 - 65, The factory
currently generates a single `id` once in GraphqlVtexContextFactory and reuses
it for all requests; move the `crypto.randomBytes(32).toString('hex')` call into
the returned closure so a fresh request-scoped ID is assigned to `ctx.id` on
each invocation (update the returned function that sets `ctx.id`, leaving other
initializations like `ctx.storage`, `ctx.clients`, `ctx.loaders`, `ctx.account`,
and `ctx.OTEL` unchanged).
packages/api/local/server.mjs (1)

11-23: ⚠️ Potential issue | 🟡 Minor

Missing OTEL property in local server apiOptions.

GraphqlVtexContextFactory now assigns ctx.OTEL = options.OTEL. Without OTEL here, ctx.OTEL will be undefined, which violates the Record<string, unknown> contract on GraphqlContext. The core options (in packages/core/src/server/options.ts) already include OTEL: {}. This local server should match.

Proposed fix
 const apiOptions = {
   platform: 'vtex',
   account: 'storeframework',
   locale: 'en-US',
   environment: 'vtexcommercestable',
   channel:
     '{"salesChannel":"1","regionId":"","hasOnlyDefaultSalesChannel":"true"}',
   showSponsored: false,
+  OTEL: {},
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/local/server.mjs` around lines 11 - 23, The apiOptions object
used to build context for GraphqlVtexContextFactory is missing the OTEL
property, causing ctx.OTEL to be undefined and violating the GraphqlContext
contract; update the apiOptions constant to include an OTEL field (e.g., OTEL:
{}) so GraphqlVtexContextFactory receives OTEL and ctx.OTEL is defined when
creating the Yoga server with GraphqlVtexSchema() and
GraphqlVtexContextFactory(apiOptions).
🧹 Nitpick comments (5)
packages/api/vite.config.mts (1)

12-12: Consider external source maps (true) instead of 'inline' for non-production.

'inline' embeds the full base64-encoded source map directly into every output file, which can balloon dist artifacts significantly in dev/watch mode. Using true (or 'hidden') generates a separate .map file instead, keeping the bundled output lean while preserving debuggability.

♻️ Proposed adjustment
-    sourcemap: mode === 'production' ? 'hidden' : 'inline',
+    sourcemap: mode === 'production' ? 'hidden' : true,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/vite.config.mts` at line 12, The sourcemap config currently uses
'inline' for non-production which embeds base64 maps into files; update the
sourcemap expression in vite.config.mts (the sourcemap option) to generate
external maps for dev by returning true instead of 'inline' (e.g., keep
production as 'hidden' but change the non-production branch to true) so source
maps are emitted as separate .map files and dist artifacts stay lean.
packages/core/src/server/options.ts (1)

27-36: as T hides the OTEL augmentation and the generic is unconstrained — type safety concern.

The function always adds an OTEL property to the return value, but the return type Promise<T> doesn't reflect that. Meanwhile, T has no extends object constraint, so spreading ...apiOptions is only safe by accident. A constrained return type would be more honest:

-export async function withTraceClient<T = typeof apiOptions>(
-  apiOptions: T
-): Promise<T> {
+export async function withTraceClient<T extends Record<string, unknown> = typeof apiOptions>(
+  options: T
+): Promise<T & { OTEL: Record<string, unknown> }> {
   const OTEL = {}
   getTraceClient(name)?.inject(OTEL)
 
   return {
-    ...apiOptions,
+    ...options,
     OTEL,
-  } as T
+  }
 }

This also renames the parameter to avoid shadowing the module-level apiOptions. As per coding guidelines, "Ensure type safety and avoid type assertions when possible".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/server/options.ts` around lines 27 - 36, Change
withTraceClient to accurately reflect that it augments the input options with an
OTEL field and to constrain the generic: make the type parameter T extend object
(e.g., <T extends object>) and change the return type to Promise<T & { OTEL:
Record<string, unknown> }> (or a more specific OTEL type), rename the parameter
(e.g., inputOptions) to avoid shadowing the module-level apiOptions, remove the
unsafe "as T" cast, and return { ...inputOptions, OTEL } typed as T & { OTEL:
... } so the compiler knows the OTEL augmentation is present; keep the
getTraceClient(name)?.inject(OTEL) call unchanged.
packages/api/test/integration/queries.test.ts (1)

114-291: Consider extracting createRunner into a shared test utility.

The createRunner function is essentially identical in queries.test.ts and mutations.test.ts (same apiOptions shape, same schema/context wiring). A shared helper would reduce duplication and make future changes to the runner setup a single-point edit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/test/integration/queries.test.ts` around lines 114 - 291, The
tests duplicate the createRunner setup across queries.test.ts and
mutations.test.ts; extract it into a shared test utility to DRY the code. Create
a new helper (e.g., export function createTestRunner or re-export createRunner)
that encapsulates the apiOptions shape and schema/context wiring used in both
files, move the existing createRunner implementation into that helper, update
queries.test.ts and mutations.test.ts to import and call the shared
createTestRunner (replacing their local createRunner), and remove the duplicated
local definitions so future changes are made in one place.
packages/api/src/platforms/vtex/index.ts (1)

67-71: Remove commented-out dead code.

The VtexResolver function is fully commented out. If it's no longer needed, remove it to keep the codebase clean. If it's planned for future use, track it in an issue instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/platforms/vtex/index.ts` around lines 67 - 71, Remove the
dead commented-out VtexResolver block: delete the commented function and its
lines referencing VtexResolver, GraphqlResolver, ResolverTrace, and
GraphqlContext; if this behavior is required later, open an issue referencing
VtexResolver and ResolverTrace instead of leaving the commented code in
packages/api/src/platforms/vtex/index.ts.
packages/api/src/observability/telemetry.ts (1)

7-14: Consider tightening the info parameter type.

info: any on lines 13 and 20 could be GraphQLResolveInfo from the graphql package. This would improve type safety and catch misuse at compile time without any runtime cost.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/observability/telemetry.ts` around lines 7 - 14, The
ResolverTrace function currently types its fourth parameter as info: any;
tighten this by importing GraphQLResolveInfo from the graphql package and
changing the parameter type in ResolverTrace (and any internal uses) to info:
GraphQLResolveInfo so callers and implementations get proper compile-time type
checking; update both the function signature (fn: (source, vars, context, info:
GraphQLResolveInfo) => TReturn) and the ResolverTrace declaration that
references info to use GraphQLResolveInfo.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/api/src/platforms/vtex/index.ts`:
- Around line 80-96: getResolvers() has two issues: the parameter name in the
map ([name, resolverFunction]) shadows the module-level name import and
finalResolvers is typed as any which defeats the final "satisfies typeof
resolvers" check. Fix by renaming the destructured variable to a non-conflicting
identifier (e.g., fieldName) in the Object.entries(...).map call so it doesn't
shadow the imported name, and give finalResolvers a proper type instead of any
(for example derive its shape from nativeGetResolvers() or use Partial<typeof
resolvers> / Record<string, typeof resolver> as appropriate) so the satisfies
typeof resolvers check is meaningful; keep wrapping resolver functions with
ResolverTrace(`${key}(${fieldName})`) and preserve use of nativeGetResolvers and
getResolvers.

In `@packages/api/vite.config.mts`:
- Line 6: The build is externalizing everything from devDependencies which
causes runtime packages to be omitted from the bundle; update the externals
logic in vite.config.mts to only use dependencies and peerDependencies (remove
devDependencies from the externals array/merge) and move runtime packages such
as `@envelop/`*, `@faststore/diagnostics`, `@opentelemetry/api`, graphql,
graphql-yoga, and tslib into package.json dependencies or peerDependencies as
appropriate, keeping only true build-time tools (vite, vitest,
`@graphql-codegen/`*) in devDependencies; reference the existing variables
dependencies, peerDependencies, devDependencies and the externals handling in
vite.config.mts when making the change.

In `@packages/diagnostics/src/config.json`:
- Around line 6-16: The "critical_endpoints" rule currently sets sampleRate: 1.0
which will trace every request to endpoints matching attribute "http.target"
value "^/api/graphql"; reduce the default sampling to a lower value (e.g., 0.1)
or make it environment-dependent by reading an env var and applying it via the
diagnostics config hot-reload mechanism so production uses a lower rate while
allowing full sampling in debug/staging; update the "critical_endpoints" entry
to use the new sampling value or env-backed value and ensure the config reload
logic respects the changed key.

In `@packages/diagnostics/src/start.ts`:
- Around line 54-55: The calls to newLogsClient() and newMetricsClient() are
being invoked without any logs/metrics exporter configuration in config.json, so
they will fall back to defaults; either add explicit logs and metrics exporter
sections to config.json (e.g., specify exporter type and options) or update the
code around newLogsClient() and newMetricsClient() to pass the intended exporter
config/options, and if the clients are intentionally left as no-ops add a
concise comment above the calls referencing newLogsClient() and
newMetricsClient() that this is deliberate and will be configured later so
reviewers understand the intent.
- Line 49: The use of require.resolve for configPath will break in ESM builds;
inside getTelemetryClient replace
require.resolve('@faststore/diagnostics/config') with an ESM-safe resolution
(either create a require via createRequire(import.meta.url).resolve(...) or use
await import.meta.resolve(...)) so the code runs in both CJS and ESM. Update the
top of the module to import createRequire from 'module' if you choose that
approach, or make the resolution async if using import.meta.resolve; ensure the
symbol configPath in getTelemetryClient uses the chosen resolver instead of
require.resolve.

In `@packages/diagnostics/vite.config.mts`:
- Line 10: Remove the unnecessary type assertion by updating the Vite config's
plugins array to use dts() directly; locate the plugins: [dts() as any] entry
and change it to plugins: [dts()] so it matches sibling packages (api,
components, sdk, ui) and removes the redundant cast.

---

Outside diff comments:
In `@packages/api/local/server.mjs`:
- Around line 11-23: The apiOptions object used to build context for
GraphqlVtexContextFactory is missing the OTEL property, causing ctx.OTEL to be
undefined and violating the GraphqlContext contract; update the apiOptions
constant to include an OTEL field (e.g., OTEL: {}) so GraphqlVtexContextFactory
receives OTEL and ctx.OTEL is defined when creating the Yoga server with
GraphqlVtexSchema() and GraphqlVtexContextFactory(apiOptions).

In `@packages/api/src/platforms/vtex/index.ts`:
- Around line 43-65: The factory currently generates a single `id` once in
GraphqlVtexContextFactory and reuses it for all requests; move the
`crypto.randomBytes(32).toString('hex')` call into the returned closure so a
fresh request-scoped ID is assigned to `ctx.id` on each invocation (update the
returned function that sets `ctx.id`, leaving other initializations like
`ctx.storage`, `ctx.clients`, `ctx.loaders`, `ctx.account`, and `ctx.OTEL`
unchanged).

---

Duplicate comments:
In `@packages/api/src/observability/telemetry.ts`:
- Around line 50-53: The returnedValue Promise handling in the wrapper doesn't
end the span on rejection, so update the Promise chain for returnedValue (the
variable returned from the resolver) to attach a .catch(...) that calls
span.end() before rethrowing the error; keep the existing .then(...) that ends
the span on success and returns the value, and ensure both success and error
paths always call span.end() (refer to returnedValue, span.end, and the
surrounding resolver wrapper function).

In `@packages/diagnostics/vite.config.mts`:
- Around line 15-17: The bundle is emitting mismatched extensions due to the
static fileName '[format]/index'; update the Vite config (the
entry/formats/fileName block in packages/diagnostics/vite.config.mts) to supply
a fileName function that returns explicit extensions per format (e.g., return
'cjs/index.cjs' for 'cjs' and 'es/index.js' for 'es') so output extensions are
pinned regardless of package.json "type"; after changing fileName, verify
packages/diagnostics/package.json "type" and the "require" export path point to
the correct extension (e.g., ./dist/cjs/index.cjs) and adjust the "require"
field if you prefer Vite's default naming instead.

---

Nitpick comments:
In `@packages/api/src/observability/telemetry.ts`:
- Around line 7-14: The ResolverTrace function currently types its fourth
parameter as info: any; tighten this by importing GraphQLResolveInfo from the
graphql package and changing the parameter type in ResolverTrace (and any
internal uses) to info: GraphQLResolveInfo so callers and implementations get
proper compile-time type checking; update both the function signature (fn:
(source, vars, context, info: GraphQLResolveInfo) => TReturn) and the
ResolverTrace declaration that references info to use GraphQLResolveInfo.

In `@packages/api/src/platforms/vtex/index.ts`:
- Around line 67-71: Remove the dead commented-out VtexResolver block: delete
the commented function and its lines referencing VtexResolver, GraphqlResolver,
ResolverTrace, and GraphqlContext; if this behavior is required later, open an
issue referencing VtexResolver and ResolverTrace instead of leaving the
commented code in packages/api/src/platforms/vtex/index.ts.

In `@packages/api/test/integration/queries.test.ts`:
- Around line 114-291: The tests duplicate the createRunner setup across
queries.test.ts and mutations.test.ts; extract it into a shared test utility to
DRY the code. Create a new helper (e.g., export function createTestRunner or
re-export createRunner) that encapsulates the apiOptions shape and
schema/context wiring used in both files, move the existing createRunner
implementation into that helper, update queries.test.ts and mutations.test.ts to
import and call the shared createTestRunner (replacing their local
createRunner), and remove the duplicated local definitions so future changes are
made in one place.

In `@packages/api/vite.config.mts`:
- Line 12: The sourcemap config currently uses 'inline' for non-production which
embeds base64 maps into files; update the sourcemap expression in
vite.config.mts (the sourcemap option) to generate external maps for dev by
returning true instead of 'inline' (e.g., keep production as 'hidden' but change
the non-production branch to true) so source maps are emitted as separate .map
files and dist artifacts stay lean.

In `@packages/core/src/server/options.ts`:
- Around line 27-36: Change withTraceClient to accurately reflect that it
augments the input options with an OTEL field and to constrain the generic: make
the type parameter T extend object (e.g., <T extends object>) and change the
return type to Promise<T & { OTEL: Record<string, unknown> }> (or a more
specific OTEL type), rename the parameter (e.g., inputOptions) to avoid
shadowing the module-level apiOptions, remove the unsafe "as T" cast, and return
{ ...inputOptions, OTEL } typed as T & { OTEL: ... } so the compiler knows the
OTEL augmentation is present; keep the getTraceClient(name)?.inject(OTEL) call
unchanged.

Comment on lines +80 to +96
export function getResolvers() {
const resolvers = nativeGetResolvers()

const finalResolvers: any = {}
for (const [key, resolver] of Object.entries(resolvers)) {
finalResolvers[key] = Object.fromEntries(
Object.entries(resolver).map(([name, resolverFunction]) => {
if (typeof resolverFunction !== 'function')
return [name, resolverFunction]

return [name, ResolverTrace(resolverFunction, `${key}(${name})`)]
})
)
}

return finalResolvers satisfies typeof resolvers
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

name shadows the module-level name import; any nullifies the satisfies check.

Two concerns in getResolvers():

  1. Line 86: The destructured name in .map(([name, resolverFunction]) shadows the name imported from package.json (line 5). Not a runtime bug here, but easy to trip over in future edits. Rename to e.g. fieldName.

  2. Lines 83/95: finalResolvers is typed any, so satisfies typeof resolvers is a no-op — it won't catch shape mismatches at compile time.

Proposed fix
-  const finalResolvers: any = {}
+  const finalResolvers: Record<string, Record<string, unknown>> = {}
   for (const [key, resolver] of Object.entries(resolvers)) {
     finalResolvers[key] = Object.fromEntries(
-      Object.entries(resolver).map(([name, resolverFunction]) => {
+      Object.entries(resolver).map(([fieldName, resolverFunction]) => {
         if (typeof resolverFunction !== 'function')
-          return [name, resolverFunction]
+          return [fieldName, resolverFunction]
 
-        return [name, ResolverTrace(resolverFunction, `${key}(${name})`)]
+        return [fieldName, ResolverTrace(resolverFunction, `${key}(${fieldName})`)]
       })
     )
   }

As per coding guidelines, "Ensure type safety and avoid type assertions when possible".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/platforms/vtex/index.ts` around lines 80 - 96,
getResolvers() has two issues: the parameter name in the map ([name,
resolverFunction]) shadows the module-level name import and finalResolvers is
typed as any which defeats the final "satisfies typeof resolvers" check. Fix by
renaming the destructured variable to a non-conflicting identifier (e.g.,
fieldName) in the Object.entries(...).map call so it doesn't shadow the imported
name, and give finalResolvers a proper type instead of any (for example derive
its shape from nativeGetResolvers() or use Partial<typeof resolvers> /
Record<string, typeof resolver> as appropriate) so the satisfies typeof
resolvers check is meaningful; keep wrapping resolver functions with
ResolverTrace(`${key}(${fieldName})`) and preserve use of nativeGetResolvers and
getResolvers.

Comment on lines +6 to +16
{
"name": "critical_endpoints",
"sampleRate": 1.0,
"conditions": [
{
"attribute": "http.target",
"operator": "Regex",
"value": "^/api/graphql"
}
]
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

100% sampling on /api/graphql may be costly at scale.

sampleRate: 1.0 means every single GraphQL request produces a trace. This is great for debugging, but in production under load the exporter overhead and backend ingestion costs can be significant. Consider starting with a lower rate (e.g., 0.1) or making it environment-dependent via the diagnostics config hot-reload capability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/diagnostics/src/config.json` around lines 6 - 16, The
"critical_endpoints" rule currently sets sampleRate: 1.0 which will trace every
request to endpoints matching attribute "http.target" value "^/api/graphql";
reduce the default sampling to a lower value (e.g., 0.1) or make it
environment-dependent by reading an env var and applying it via the diagnostics
config hot-reload mechanism so production uses a lower rate while allowing full
sampling in debug/staging; update the "critical_endpoints" entry to use the new
sampling value or env-backed value and ensure the config reload logic respects
the changed key.

},
debug: globalThis.fsDiagnostics.IS_DEV,
config: {
configPath: require.resolve('@faststore/diagnostics/config'),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check the diagnostics package structure and configuration
echo "=== Package.json content ==="
cat packages/diagnostics/package.json

echo ""
echo "=== start.ts file content (full) ==="
cat packages/diagnostics/src/start.ts

Repository: vtex/faststore

Length of output: 3082


🏁 Script executed:

# Check for vite config in diagnostics package
echo "=== Vite config (if exists) ==="
fd "vite\.config" packages/diagnostics --exec cat {}

echo ""
echo "=== tsconfig.json in diagnostics ==="
fd "tsconfig" packages/diagnostics --exec cat {}

echo ""
echo "=== Check if require is imported/shimmed anywhere ==="
rg "createRequire|require\.resolve" packages/diagnostics/src/ -A 2 -B 2

Repository: vtex/faststore

Length of output: 1543


🏁 Script executed:

# Check the main entry point
echo "=== index.ts (main entry) ==="
cat packages/diagnostics/src/index.ts

echo ""
echo "=== Check how start.ts is used ==="
rg "start\.ts|from.*start" packages/diagnostics/src/ -B 2 -A 2

Repository: vtex/faststore

Length of output: 1068


require.resolve will fail in ESM builds—use createRequire or import.meta.resolve.

This file mixes ESM syntax (import ... with { type: 'json' } at line 11) with the CJS API require.resolve. The Vite config builds both CJS and ESM outputs (formats: ['es', 'cjs']), and getTelemetryClient (which contains the require.resolve call) is exported from the main entry point. When the ESM build is consumed, require will be undefined at runtime and throw an error.

Use createRequire(import.meta.url).resolve(...) or import.meta.resolve (Node 20+) to ensure the code works in both module formats.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/diagnostics/src/start.ts` at line 49, The use of require.resolve for
configPath will break in ESM builds; inside getTelemetryClient replace
require.resolve('@faststore/diagnostics/config') with an ESM-safe resolution
(either create a require via createRequire(import.meta.url).resolve(...) or use
await import.meta.resolve(...)) so the code runs in both CJS and ESM. Update the
top of the module to import createRequire from 'module' if you choose that
approach, or make the resolution async if using import.meta.resolve; ensure the
symbol configPath in getTelemetryClient uses the chosen resolver instead of
require.resolve.

Comment on lines +54 to +55
await client.newLogsClient()
await client.newMetricsClient()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

newLogsClient() and newMetricsClient() called without exporter config and no logs/metrics section in config.json.

The config at packages/diagnostics/src/config.json only defines traces.sampling — there's no logs or metrics exporter configuration. These clients will fall back to library defaults (likely no-op or console). If you intend to export logs and metrics, add the appropriate sections to config.json. If they're intentionally no-op placeholders for now, a brief comment would clarify intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/diagnostics/src/start.ts` around lines 54 - 55, The calls to
newLogsClient() and newMetricsClient() are being invoked without any
logs/metrics exporter configuration in config.json, so they will fall back to
defaults; either add explicit logs and metrics exporter sections to config.json
(e.g., specify exporter type and options) or update the code around
newLogsClient() and newMetricsClient() to pass the intended exporter
config/options, and if the clients are intentionally left as no-ops add a
concise comment above the calls referencing newLogsClient() and
newMetricsClient() that this is deliberate and will be configured later so
reviewers understand the intent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant