diff --git a/.github/workflows/back-merge.yml b/.github/workflows/back-merge.yml index daf1c18c53..59c70dee52 100644 --- a/.github/workflows/back-merge.yml +++ b/.github/workflows/back-merge.yml @@ -106,8 +106,13 @@ jobs: if: steps.prep.outputs.skip == 'false' run: | pnpm i + # core#generate shells out to the CLI binary, which must be compiled + # first (see turbo.json: @faststore/core#generate → @faststore/cli#build). + # api must be built before the CLI because generate-types imports + # GraphqlVtexSchema from @faststore/api at runtime. pnpm --filter @faststore/api build pnpm --filter @faststore/api generate + pnpm --filter @faststore/cli build pnpm --filter @faststore/core generate git add -A diff --git a/CHANGELOG.md b/CHANGELOG.md index 879e1493bc..de15d7bb76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,87 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1) (2026-08-11) +# [4.6.0-dev.6](https://github.com/vtex/faststore/compare/v4.6.0-dev.5...v4.6.0-dev.6) (2026-08-14) + +### Bug Fixes + +- resolve node_modules bins when running scripts inside .faststore ([#3440](https://github.com/vtex/faststore/issues/3440)) ([2bf174a](https://github.com/vtex/faststore/commit/2bf174a7295d106354d97d7ca2c161ed706cff9a)), closes [#3439](https://github.com/vtex/faststore/issues/3439) [#3419](https://github.com/vtex/faststore/issues/3419) + +# [4.6.0-dev.5](https://github.com/vtex/faststore/compare/v4.6.0-dev.4...v4.6.0-dev.5) (2026-08-13) + +### Bug Fixes + +- **core:** add experimental.revalidate404 and skip PLP query on notFound ([855a3ee](https://github.com/vtex/faststore/commit/855a3ee2e3393ee092d4b261ad83a8757289e132)) +- **core:** revalidate ISR cache for notFound PDP/PLP paths ([1d1015e](https://github.com/vtex/faststore/commit/1d1015e8874d794c5f54e572afc3c676fabce69c)) + +# [4.6.0-dev.4](https://github.com/vtex/faststore/compare/v4.6.0-dev.3...v4.6.0-dev.4) (2026-08-12) + +### Bug Fixes + +- custom GraphQL typeDefs silently ignored on Windows ([#3419](https://github.com/vtex/faststore/issues/3419)) ([bdeb6a7](https://github.com/vtex/faststore/commit/bdeb6a757c44dda24b96cffc73d34652de165adf)) + +# [4.6.0-dev.3](https://github.com/vtex/faststore/compare/v4.6.0-dev.2...v4.6.0-dev.3) (2026-08-12) + +**Note:** Version bump only for package faststore + +# [4.6.0-dev.2](https://github.com/vtex/faststore/compare/v4.6.0-dev.1...v4.6.0-dev.2) (2026-08-12) + +### Bug Fixes + +- **api:** address Sonar and harden session sales-channel sync ([5f6e8e9](https://github.com/vtex/faststore/commit/5f6e8e909def75f60c24cdaffae9303ed2c0a052)) +- **api:** address Sonar findings in validateCart and orderForm URL ([3211231](https://github.com/vtex/faststore/commit/321123179d3e5789fb7730ef94c8f2f0d8b878b1)) +- **api:** keep orderForm sales channel after stale validateCart sync ([09b051b](https://github.com/vtex/faststore/commit/09b051b80aaff7183adf7f5e86aece438bcd38f0)) +- **api:** preserve orderForm sales channel in validateCart ([2336cda](https://github.com/vtex/faststore/commit/2336cda2c95970850dd0d29c1c18043ca8c9078a)) +- **api:** sync adopted orderForm sales channel into session ([8943da7](https://github.com/vtex/faststore/commit/8943da7821f8abec08a39b7907a258bf156b05ee)) + +### Reverts + +- Revert "docs(api): add before/after screenshots for validateCart PR" ([eaaf032](https://github.com/vtex/faststore/commit/eaaf0323b004988d849679ba19ff61ab75dd0875)) + +# [4.6.0-dev.1](https://github.com/vtex/faststore/compare/v4.6.0-dev.0...v4.6.0-dev.1) (2026-08-11) + +## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.5.1) (2026-08-11) + +### Bug Fixes + +- **core:** address scroll restoration review feedback ([a5fe6a3](https://github.com/vtex/faststore/commit/a5fe6a35c54e6095f81c7995a815ce6ef3b4dd4e)) +- **core:** align beforePopState callback with NextHistoryState ([10719e5](https://github.com/vtex/faststore/commit/10719e5eca199385d9fe18bdd6d7da290716a1d5)) +- **core:** compare window to undefined directly for Sonar ([bf05a54](https://github.com/vtex/faststore/commit/bf05a54bb014d120b7bdc95ff7c01a4e9b35b485)) +- **core:** harden scroll restore path matching and hydration-safe heights ([34efa6b](https://github.com/vtex/faststore/commit/34efa6bda7549895a4531277c2fe55bd6bbe98a7)) +- **core:** hash gallery height keys to avoid truncation collisions ([9e98043](https://github.com/vtex/faststore/commit/9e980438d74e457fa33aa77900438339463a078c)) +- **core:** prevent IS redirect hang and improve scroll restoration ([5f30778](https://github.com/vtex/faststore/commit/5f30778161eec2e4381b3fe971479dd7cbe53b08)) +- **core:** reset infinite scroll on forward listing navigations ([249226a](https://github.com/vtex/faststore/commit/249226a3acbe4cb26d687cf962125b2593d5949e)) +- **core:** resolve Sonar new-code smells for scroll restoration ([59b41ed](https://github.com/vtex/faststore/commit/59b41ed1d2a30ba693726b19895a0279eb1ba464)) +- **core:** restore PLP infinite-scroll pages on back navigation ([92b2efa](https://github.com/vtex/faststore/commit/92b2efa1d4d0895a10b5a0e71a4d9bf37cae1686)) +- **core:** resync gallery height lock when children change ([5058089](https://github.com/vtex/faststore/commit/50580898d97baf800b97c8a876f7967529859c61)) +- **core:** satisfy Sonar on gallery height key hash ([3a6ce47](https://github.com/vtex/faststore/commit/3a6ce47485c26be75d6d76906df3af64c9e34fd6)) +- **core:** type scroll restore paint timer for DOM/Node setTimeout ([940c903](https://github.com/vtex/faststore/commit/940c90376f8d2eb77665e73a6fb0ea53c9786c8e)) +- **core:** use replaceAll for infinite-scroll session key ([4f52048](https://github.com/vtex/faststore/commit/4f520482a9ba9661782fcf25a286b8f2e6c6b922)) + +# [4.6.0-dev.0](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.6.0-dev.0) (2026-08-11) + +### Features + +- removing vtex/diagnostics-nodejs ([#3389](https://github.com/vtex/faststore/issues/3389)) ([74e2d50](https://github.com/vtex/faststore/commit/74e2d503a44903929b90af7701956536690d4e41)) + +## [4.5.1-dev.4](https://github.com/vtex/faststore/compare/v4.5.1-dev.3...v4.5.1-dev.4) (2026-08-10) ### Bug Fixes +- restore account, path, port arg order on cli dev command ([#3439](https://github.com/vtex/faststore/issues/3439)) ([b13e834](https://github.com/vtex/faststore/commit/b13e8341539fb8108c35a63f26218c4671d308f8)), closes [#3111](https://github.com/vtex/faststore/issues/3111) [#2491](https://github.com/vtex/faststore/issues/2491) [#2491](https://github.com/vtex/faststore/issues/2491) [#3111](https://github.com/vtex/faststore/issues/3111) + +## [4.5.1-dev.3](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1-dev.3) (2026-08-10) + +### Bug Fixes + +- **core:** add twitter card meta tag to default next seo config ([720f3e6](https://github.com/vtex/faststore/commit/720f3e6d04ef523f2fcf0e146c3741b7e5af70fa)) +- **core:** add twittermeta tags validaton for tests ([c8f5872](https://github.com/vtex/faststore/commit/c8f5872e42b69ddae5b2abfa0e8d3140e2bc7163)) +- **core:** lint error ([d07460f](https://github.com/vtex/faststore/commit/d07460f890ba712c9aecbec25bda5de96bf3e4ef)) +- **core:** remove duplicated twitter meta tag ([f388288](https://github.com/vtex/faststore/commit/f388288731ff75fceedba3cd7449ef1088b0b0c2)) +- **metaTags:** add twitter meta tags to nextSeo ([9c2439b](https://github.com/vtex/faststore/commit/9c2439b4a668dd81b7bfe5bdc44df575725067ec)) + +## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1) (2026-08-11) + - **core:** address scroll restoration review feedback ([a5fe6a3](https://github.com/vtex/faststore/commit/a5fe6a35c54e6095f81c7995a815ce6ef3b4dd4e)) - **core:** align beforePopState callback with NextHistoryState ([10719e5](https://github.com/vtex/faststore/commit/10719e5eca199385d9fe18bdd6d7da290716a1d5)) - **core:** compare window to undefined directly for Sonar ([bf05a54](https://github.com/vtex/faststore/commit/bf05a54bb014d120b7bdc95ff7c01a4e9b35b485)) diff --git a/README.md b/README.md index 5d98c004af..3f83be0de8 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ These packages are part of the monorepo but are not intended for direct use by s | Package | Description | | :--- | :--- | | `@faststore/lighthouse` | Generates Lighthouse CI configurations for automated performance audits and quality gates. | -| `@faststore/diagnostics` | OpenTelemetry tracing and observability for monitoring application performance. | +| `@faststore/diagnostics` | OpenTelemetry tracing and logging for monitoring application performance. | | `@faststore/storybook` | Isolated environment for developing and documenting UI components (not published). | ## License diff --git a/docs/observability.md b/docs/observability.md new file mode 100644 index 0000000000..e83822c1d6 --- /dev/null +++ b/docs/observability.md @@ -0,0 +1,51 @@ +# Observability + +FastStore uses OpenTelemetry for server-side logs and traces, exported over OTLP +gRPC to VTEX collectors. The SDK is started by `@faststore/diagnostics` from the +Next.js instrumentation hook in `@faststore/core`. + +## Enabling + +Telemetry is **off until the feature is released**. It is gated by two hardcoded +flags rather than by store configuration, so a store cannot turn it on: + +| Flag | File | Controls | +| :--- | :--- | :--- | +| `instrumentationEnabled` | [`packages/core/src/instrumentation.ts`](../packages/core/src/instrumentation.ts) | Starting the OTel SDK, and therefore whether `logger()` emits anything | +| `OTEL_ENABLED` | [`packages/core/src/server/options.ts`](../packages/core/src/server/options.ts) | The per-request root span in `execute()` and the resolver spans from `@faststore/api` | + +Flip both to `true` to exercise the pipeline locally. `logger()` is a no-op +until the SDK has started, so `OTEL_ENABLED` on its own emits neither traces nor +logs. + +## Configuration + +Endpoints and sampling default to the VTEX collectors and can be overridden with +environment variables — see +[`packages/diagnostics/src/globals.ts`](../packages/diagnostics/src/globals.ts): + +| Variable | Description | +| :--- | :--- | +| `OTLP_TRACES_ENDPOINT` | OTLP gRPC endpoint for traces | +| `OTLP_LOGGER_ENDPOINT` | OTLP gRPC endpoint for logs | +| `OTLP_TRACES_SAMPLE_RATE` | Fraction of traces exported outside development (default `0.3`) | + +To send logs to the development collector, set `OTLP_LOGGER_ENDPOINT` to +`developer-logs.opentelemetry-collector.vtex.systems:80`. + +## Logs + +Search in the [Grafana app](https://grafana.vtex.com/explore) by attribute: + +- `vtex.account.name:` — for example `vtex.account.name:storeframework` +- `vtex.application.id:faststore` + +> Development logs are queried at `victoria-logs-developer`, production at `victoria-logs-main`. + +## Traces + +Connect to the VPN and open the [SigNoz app](https://signoz-traces.vtex.systems/home), +then search for traces carrying: + +- `@faststore_account_name: ` +- `@faststore_environment: production` or `@faststore_environment: development` diff --git a/lerna.json b/lerna.json index 21cdb9a179..8164586eb9 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "4.5.1", + "version": "4.6.0-dev.6", "npmClient": "pnpm", "command": { "publish": { diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 1781a5419a..1c6d600222 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -3,6 +3,50 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.6.0-dev.6](https://github.com/vtex/faststore/compare/v4.6.0-dev.5...v4.6.0-dev.6) (2026-08-14) + +**Note:** Version bump only for package @faststore/api + +# [4.6.0-dev.5](https://github.com/vtex/faststore/compare/v4.6.0-dev.4...v4.6.0-dev.5) (2026-08-13) + +**Note:** Version bump only for package @faststore/api + +# [4.6.0-dev.4](https://github.com/vtex/faststore/compare/v4.6.0-dev.3...v4.6.0-dev.4) (2026-08-12) + +**Note:** Version bump only for package @faststore/api + +# [4.6.0-dev.3](https://github.com/vtex/faststore/compare/v4.6.0-dev.2...v4.6.0-dev.3) (2026-08-12) + +**Note:** Version bump only for package @faststore/api + +# [4.6.0-dev.2](https://github.com/vtex/faststore/compare/v4.6.0-dev.1...v4.6.0-dev.2) (2026-08-12) + +### Bug Fixes + +- **api:** address Sonar and harden session sales-channel sync ([5f6e8e9](https://github.com/vtex/faststore/commit/5f6e8e909def75f60c24cdaffae9303ed2c0a052)) +- **api:** address Sonar findings in validateCart and orderForm URL ([3211231](https://github.com/vtex/faststore/commit/321123179d3e5789fb7730ef94c8f2f0d8b878b1)) +- **api:** keep orderForm sales channel after stale validateCart sync ([09b051b](https://github.com/vtex/faststore/commit/09b051b80aaff7183adf7f5e86aece438bcd38f0)) +- **api:** preserve orderForm sales channel in validateCart ([2336cda](https://github.com/vtex/faststore/commit/2336cda2c95970850dd0d29c1c18043ca8c9078a)) +- **api:** sync adopted orderForm sales channel into session ([8943da7](https://github.com/vtex/faststore/commit/8943da7821f8abec08a39b7907a258bf156b05ee)) + +# [4.6.0-dev.1](https://github.com/vtex/faststore/compare/v4.6.0-dev.0...v4.6.0-dev.1) (2026-08-11) + +## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.5.1) (2026-08-11) + +**Note:** Version bump only for package @faststore/api + +# [4.6.0-dev.0](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.6.0-dev.0) (2026-08-11) + +### Features + +- removing vtex/diagnostics-nodejs ([#3389](https://github.com/vtex/faststore/issues/3389)) ([74e2d50](https://github.com/vtex/faststore/commit/74e2d503a44903929b90af7701956536690d4e41)) + +## [4.5.1-dev.4](https://github.com/vtex/faststore/compare/v4.5.1-dev.3...v4.5.1-dev.4) (2026-08-10) + +**Note:** Version bump only for package @faststore/api + +## [4.5.1-dev.3](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1-dev.3) (2026-08-10) + ## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1) (2026-08-11) **Note:** Version bump only for package @faststore/api diff --git a/packages/api/package.json b/packages/api/package.json index ea355df22d..1016b45a39 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/api", - "version": "4.5.1", + "version": "4.6.0-dev.6", "license": "MIT", "main": "dist/cjs/index.js", "typings": "dist/src/index.d.ts", @@ -71,7 +71,7 @@ "tslib": "catalog:", "vite": "catalog:", "vite-plugin-dts": "catalog:", - "vitest": "^4.0.7" + "vitest": "catalog:" }, "peerDependencies": { "@faststore/diagnostics": "workspace:*", diff --git a/packages/api/src/__generated__/schema.ts b/packages/api/src/__generated__/schema.ts index 8889e0f090..dec26def8d 100644 --- a/packages/api/src/__generated__/schema.ts +++ b/packages/api/src/__generated__/schema.ts @@ -1616,6 +1616,12 @@ export type StoreOrder = { acceptedOffer: Array; /** ID of the order in [VTEX order management](https://help.vtex.com/en/tutorial/license-manager-resources-oms--60QcBsvWeum02cFi3GjBzg#). */ orderNumber: Scalars['String']['output']; + /** + * Sales channel of the underlying orderForm when FastStore adopted it because + * the browser session was stale (e.g. after Quick Order). Clients should align + * `session.channel` to this value. Null when no SC adoption happened. + */ + salesChannel?: Maybe; /** Indicates whether or not items with attachments should be split. */ shouldSplitItem?: Maybe; }; diff --git a/packages/api/src/observability/telemetry.ts b/packages/api/src/observability/telemetry.ts index 7cb5af01be..4d7fa40a21 100644 --- a/packages/api/src/observability/telemetry.ts +++ b/packages/api/src/observability/telemetry.ts @@ -1,9 +1,11 @@ -import { OTELAPI } from '@faststore/diagnostics' +import { logger, OTELAPI } from '@faststore/diagnostics' import { name, version } from '../../package.json' with { type: 'json' } +const OTELLogger = logger('@faststore/api') + export const ResolverTrace = < TContext extends { - OTEL: Record + OTEL_ENABLED: boolean account: string discoveryConfig: Record }, @@ -12,7 +14,7 @@ export const ResolverTrace = < TReturn = any, >( fn: (source: TSource, vars: TVars, context: TContext, info: any) => TReturn, - resolverName?: string + resolverName = 'Unknown Graphql Resolver' ) => { return ( source: TSource, @@ -20,47 +22,62 @@ export const ResolverTrace = < graphqlContext: TContext, info: any ): TReturn => { - if ((graphqlContext?.OTEL?.enabled ?? false) === false) { + if (!graphqlContext?.OTEL_ENABLED) { return fn(source, vars, graphqlContext, info) } - const trace = OTELAPI.trace.getTracer(name, version) - const activeContext = OTELAPI.propagation.extract( - OTELAPI.context.active(), - graphqlContext.OTEL - ) + const span = OTELAPI.trace.getTracer('Graphql').startSpan(resolverName, { + kind: OTELAPI.SpanKind.INTERNAL, + attributes: { + timestamp: Date.now(), + '@faststore_version': version, + '@faststore_package_name': name, + '@faststore_account_name': graphqlContext.account, + '@faststore_environment': process.env.NODE_ENV, + '@faststore_resolver_parent_type': info?.parentType?.name, + '@faststore_resolver_field_name': info?.fieldName, + }, + }) - return OTELAPI.context.with(activeContext, () => { - const span = trace.startSpan(resolverName ?? 'Unknown Graphql Resolver', { - startTime: Date.now(), - kind: OTELAPI.SpanKind.INTERNAL, - attributes: { - timestamp: Date.now(), - '@faststore_version': version, - '@faststore_package_name': name, - '@faststore_account_name': graphqlContext.account, - '@faststore_environment': process.env.NODE_ENV, - }, - }) + const otelContext = OTELAPI.trace.setSpan(OTELAPI.context.active(), span) - if (!span) return fn(source, vars, graphqlContext, info) - try { - const returnedValue = fn(source, vars, graphqlContext, info) + try { + const result = OTELAPI.context.with(otelContext, () => + fn(source, vars, graphqlContext, info) + ) - if (!(returnedValue instanceof Promise)) { - span.end() - return returnedValue - } + if (result instanceof Promise) { + return result.then( + (value) => { + span.end() - return returnedValue.then((value) => { - span.end() - return value - }) as TReturn - } catch (error) { - span.end() - console.error(`Error when executing resolver: ${resolverName}`, error) - throw error + return value + }, + (error) => recordResolverError(error, span, resolverName) + ) as TReturn } - }) + + span.end() + + return result + } catch (error) { + return recordResolverError(error, span, resolverName) + } } } + +/** Marks the span as failed and rethrows, keeping call sites linear. */ +function recordResolverError( + error: unknown, + span: OTELAPI.Span, + resolverName: string +): never { + span.setStatus({ code: OTELAPI.SpanStatusCode.ERROR }) + span.recordException(error as Error) + span.end() + + console.error(`Error at resolver: ${resolverName}: \n %o`, error) + OTELLogger('error', 'Error at resolver: %s\n%o', resolverName, error) + + throw error +} diff --git a/packages/api/src/platforms/vtex/clients/commerce/index.ts b/packages/api/src/platforms/vtex/clients/commerce/index.ts index 3e67dc7f37..e46c3e888c 100644 --- a/packages/api/src/platforms/vtex/clients/commerce/index.ts +++ b/packages/api/src/platforms/vtex/clients/commerce/index.ts @@ -435,21 +435,36 @@ export const VtexCommerce = ( id, refreshOutdatedData = true, channel = ctx.storage.channel, + preserveSalesChannel = false, }: { id?: string refreshOutdatedData?: boolean channel?: Required + /** + * When true, omit the `sc` query param so Checkout keeps the cart's + * current sales channel. Use this when the session SC may be stale + * relative to an external flow (e.g. Quick Order) that already + * advanced the orderForm. + */ + preserveSalesChannel?: boolean }): Promise => { const { salesChannel } = channel const headers: HeadersInit = withCookie({ 'content-type': 'application/json', 'X-FORWARDED-HOST': forwardedHost, }) - const params = new URLSearchParams({ sc: salesChannel }) + const params = new URLSearchParams() + // New carts (no id) always need an explicit SC. Existing carts may + // omit it so Checkout does not recalculate under a stale session SC. + if (!preserveSalesChannel || !id) { + params.set('sc', salesChannel) + } if (id) { params.set('refreshOutdatedData', refreshOutdatedData.toString()) } - const url = `${base}/api/checkout/pub/orderForm${id ? `/${id}` : ''}?${params.toString()}` + const orderFormPath = id ? `/${id}` : '' + // Always has at least `sc` (new cart) or `refreshOutdatedData` (existing). + const url = `${base}/api/checkout/pub/orderForm${orderFormPath}?${params}` return fetchAPI( url, diff --git a/packages/api/src/platforms/vtex/index.ts b/packages/api/src/platforms/vtex/index.ts index b9c878e54c..10b8a655c6 100644 --- a/packages/api/src/platforms/vtex/index.ts +++ b/packages/api/src/platforms/vtex/index.ts @@ -1,4 +1,3 @@ -import { OTELAPI } from '@faststore/diagnostics' import { mergeSchemas } from '@graphql-tools/schema' import { type GraphQLSchema, isSchema } from 'graphql' import { withDirectives } from '../../directives' @@ -45,34 +44,30 @@ export interface GraphqlContext { } headers: Record account: string - OTEL: Record + OTEL_ENABLED: boolean /** Discovery config passed from @faststore/core, including localization settings. */ discoveryConfig?: Record } export const GraphqlVtexContextFactory = async (options: Options) => { - return OTELAPI.context.with( - OTELAPI.propagation.extract(OTELAPI.context.active(), options.OTEL), - () => - (ctx: any): GraphqlContext => { - ctx.storage = { - channel: ChannelMarshal.parse(options.channel), - flags: options.flags ?? {}, - locale: options.locale, - cookies: new Map>(), - } - ctx.account = options.account - ctx.OTEL = options.OTEL - ctx.discoveryConfig = options.discoveryConfig - // Build clients/loaders last: they capture `ctx` and read storage, - // discoveryConfig, etc. at request time, so everything they may depend on - // must already be assigned. - ctx.clients = getClients(options, ctx) - ctx.loaders = getLoaders(options, ctx) + return (ctx: any): GraphqlContext => { + ctx.storage = { + channel: ChannelMarshal.parse(options.channel), + flags: options.flags ?? {}, + locale: options.locale, + cookies: new Map>(), + } + ctx.account = options.account + ctx.OTEL_ENABLED = options.OTEL_ENABLED + ctx.discoveryConfig = options.discoveryConfig + // Build clients/loaders last: they capture `ctx` and read storage, + // discoveryConfig, etc. at request time, so everything they may depend on + // must already be assigned. + ctx.clients = getClients(options, ctx) + ctx.loaders = getLoaders(options, ctx) - return ctx - } - ) + return ctx + } } export type GraphqlResolver = Resolver< diff --git a/packages/api/src/platforms/vtex/resolvers/validateCart.ts b/packages/api/src/platforms/vtex/resolvers/validateCart.ts index 8d0b570ee3..105deedb27 100644 --- a/packages/api/src/platforms/vtex/resolvers/validateCart.ts +++ b/packages/api/src/platforms/vtex/resolvers/validateCart.ts @@ -1,6 +1,7 @@ import deepEquals from 'fast-deep-equal' import { parse } from 'cookie' +import { channelWhenSessionDivergesFromOrderForm } from '../utils/cartSalesChannel' import { mutateChannelContext, mutateLocaleContext } from '../utils/contex' import { md5 } from '../utils/md5' import { @@ -175,7 +176,8 @@ const joinItems = (form: OrderForm) => { const orderFormToCart = async ( form: OrderForm, skuLoader: GraphqlContext['loaders']['skuLoader'], - shouldSplitItem?: boolean | null + shouldSplitItem?: boolean | null, + adoptedSalesChannel?: string | null ) => { return { order: { @@ -185,6 +187,7 @@ const orderFormToCart = async ( product: await skuLoader.load(`${item.id}-invisibleItems`), })), shouldSplitItem, + ...(adoptedSalesChannel ? { salesChannel: adoptedSalesChannel } : {}), }, messages: form.messages.map(({ text, status }) => ({ text, @@ -336,6 +339,38 @@ const getCookieCheckoutOrderNumber = (ctx: string, nameCookie: string) => { return cookieValue ? cookieValue.split('=')[1] : '' } +/** Keep Checkout on the orderForm SC when the browser session lags behind it. */ +const adoptOrderFormSalesChannelWhenSessionDiverges = ( + ctx: GraphqlContext, + orderForm: OrderForm +): string | null => { + const adoptedChannel = channelWhenSessionDivergesFromOrderForm( + ctx.storage.channel, + orderForm.salesChannel + ) + + if (adoptedChannel) { + mutateChannelContext(ctx, adoptedChannel) + return orderForm.salesChannel ? String(orderForm.salesChannel) : null + } + + return null +} + +/** Return a cart only when an SC was adopted (so the client can sync session). */ +const cartWhenSalesChannelAdoptedOrNull = ( + form: OrderForm, + skuLoader: GraphqlContext['loaders']['skuLoader'], + shouldSplitItem: boolean | null | undefined, + adoptedSalesChannel: string | null +) => { + if (!adoptedSalesChannel) { + return null + } + + return orderFormToCart(form, skuLoader, shouldSplitItem, adoptedSalesChannel) +} + /** * This resolver implements the optimistic cart behavior. The main idea in here * is that we receive a cart from the UI (as query params) and we validate it with @@ -374,10 +409,17 @@ export const validateCart = async ( mutateLocaleContext(ctx, locale) } - // Step1: Get OrderForm from VTEX Commerce + // Step1: Get OrderForm from VTEX Commerce. + // For existing carts (`orderFormId` present), omit `sc` on the first GET so + // Checkout keeps the orderForm's current sales channel. Passing a stale + // session SC (e.g. after Quick Order) would recalculate the cart and drop + // items only available in the orderForm's trade policy. New carts still + // send `sc` from the session (see commerce.checkout.orderForm). + const orderFormId = orderFormIdFromCookie || undefined const orderForm = await commerce.checkout.orderForm({ - id: orderFormIdFromCookie || undefined, + id: orderFormId, channel: ctx.storage.channel, + preserveSalesChannel: Boolean(orderFormId), }) const orderNumber = orderForm.orderFormId @@ -398,16 +440,36 @@ export const validateCart = async ( const isStale = isOrderFormStale(orderForm, sessionJwt) if (isStale) { + // Adopt the orderForm SC so subsequent checkout calls (etag, etc.) stay + // on the trade policy that actually owns the items. + const adoptedSalesChannel = adoptOrderFormSalesChannelWhenSessionDiverges( + ctx, + orderForm + ) + const newOrderForm = await setOrderFormEtag( orderForm, commerce, sessionJwt ).then(joinItems) if (orderNumber) { - return orderFormToCart(newOrderForm, skuLoader, shouldSplitItem) + return orderFormToCart( + newOrderForm, + skuLoader, + shouldSplitItem, + adoptedSalesChannel + ) } } + // Keep Checkout on the orderForm trade policy when the browser session still + // has a stale SC (Quick Order). Refetching with `sc=session` would wipe + // items that exist only on the orderForm's sales channel. + const adoptedSalesChannel = adoptOrderFormSalesChannelWhenSessionDiverges( + ctx, + orderForm + ) + // Step2: Process items from both browser and checkout so they have the same shape const browserItemsById = groupById(acceptedOffer) const originItemsById = groupById(orderForm.items.map(orderFormItemToOffer)) @@ -470,9 +532,15 @@ export const validateCart = async ( ? shouldUpdateShippingData(orderForm, session) : { updateShipping: false } - // If there are no item changes and no shipping data updates needed, return null + // If there are no item/shipping changes: still return the cart when we + // adopted the orderForm SC so the client can align `fs::session`. if (changes.length === 0 && !updateShipping) { - return null + return cartWhenSalesChannelAdoptedOrNull( + orderForm, + skuLoader, + shouldSplitItem, + adoptedSalesChannel + ) } // Step4: Apply delta changes to order form @@ -541,9 +609,19 @@ export const validateCart = async ( // Step5: If no changes detected before/after updating orderForm, the order is validated if (equals(order, updatedOrderForm) && equalMessages) { - return null + return cartWhenSalesChannelAdoptedOrNull( + updatedOrderForm, + skuLoader, + shouldSplitItem, + adoptedSalesChannel + ) } // Step6: There were changes, convert orderForm to StoreCart - return orderFormToCart(updatedOrderForm, skuLoader, shouldSplitItem) + return orderFormToCart( + updatedOrderForm, + skuLoader, + shouldSplitItem, + adoptedSalesChannel + ) } diff --git a/packages/api/src/platforms/vtex/resolvers/validateSession.ts b/packages/api/src/platforms/vtex/resolvers/validateSession.ts index 0b5e474a45..150a7c5ad6 100644 --- a/packages/api/src/platforms/vtex/resolvers/validateSession.ts +++ b/packages/api/src/platforms/vtex/resolvers/validateSession.ts @@ -3,39 +3,29 @@ import deepEquals from 'fast-deep-equal' import type { GraphqlContext } from '..' import type { MutationValidateSessionArgs, - StoreMarketingData, StoreSession, } from '../../../__generated__/schema' import ChannelMarshal from '../utils/channel' -import { resolveActiveContractDisplayName } from '../utils/contract' -import { getAuthCookie, parseJwt } from '../utils/cookies' - -async function getPreciseLocationData( - clients: GraphqlContext['clients'], - country: string, - postalCode: string -) { - try { - const address = await clients.commerce.checkout.address({ - postalCode, - country, - }) - - const geoCoordinates = address.geoCoordinates - ? { - latitude: address.geoCoordinates[1], - longitude: address.geoCoordinates[0], - } - : null - - return { city: address.city, geoCoordinates } - } catch (err) { - console.error( - `Error while getting geo coordinates for the current postal code (${postalCode}) and country (${country}).\n` - ) +import { channelAfterSessionManager } from '../utils/sessionChannel' +import { + buildB2bSession, + buildMarketingData, + buildPersonFromProfile, + buildSessionSearchParams, + getPreciseLocationData, + resolveJwtClaims, + resolveSellerInRegion, +} from '../utils/validateSessionHelpers' + +type SessionStoreNamespace = { + channel?: { value?: string | null } | null + currencyCode?: { value?: string | null } | null + currencySymbol?: { value?: string | null } | null + countryCode?: { value?: string | null } | null +} - throw err - } +type SessionCheckoutNamespace = { + regionId?: { value?: string | null } | null } export const validateSession = async ( @@ -49,7 +39,6 @@ export const validateSession = async ( let city = oldSession.city ?? null let geoCoordinates = oldSession.geoCoordinates ?? null - // Update location data if postal code and country are provided const shouldGetPreciseLocation = !city || !geoCoordinates if (shouldGetPreciseLocation && postalCode !== '' && country !== '') { const preciseLocation = await getPreciseLocationData( @@ -65,64 +54,20 @@ export const validateSession = async ( * The Session Manager API (https://developers.vtex.com/docs/api-reference/session-manager-api#patch-/api/sessions) adds the query params to the session public namespace. * This is used by Checkout (checkout-session) and Intelligent Search (search-session) */ - const params = new URLSearchParams(search) - - // Remove facets parameter if it exists so that it does not interfere with session data and prioritize vtex_segment - if (params.has('facets')) { - params.delete('facets') - } - - const salesChannel = params.get('sc') ?? channel.salesChannel - params.set('sc', salesChannel) - - if (!!postalCode) { - params.set('postalCode', postalCode) - } - - if (!!country) { - params.set('country', country) - } - - if (!!geoCoordinates) { - params.set( - 'geoCoordinates', - `${geoCoordinates.longitude},${geoCoordinates.latitude}` // long,lat is the format expected - ) - } - - // Sending the locale to the session, the store-session app will update cultureInfo - params.set('locale', oldSession.locale) - - const { marketingData: oldMarketingData } = oldSession - - const marketingData: StoreMarketingData = { - utmCampaign: - params.get('utm_campaign') ?? oldMarketingData?.utmCampaign ?? '', - utmMedium: params.get('utm_medium') ?? oldMarketingData?.utmMedium ?? '', - utmSource: params.get('utm_source') ?? oldMarketingData?.utmSource ?? '', - utmiCampaign: params.get('utmi_cp') ?? oldMarketingData?.utmiCampaign ?? '', - utmiPage: params.get('utmi_p') ?? oldMarketingData?.utmiPage ?? '', - utmiPart: params.get('utmi_pc') ?? oldMarketingData?.utmiPart ?? '', - } - - const jwt = parseJwt(getAuthCookie(headers?.cookie ?? '', account)) - - // Validate JWT token if it exists - let isValidJwt = false - if (jwt) { - try { - const vtexIdResponse = await clients.commerce.vtexid.validate() - isValidJwt = vtexIdResponse?.authStatus?.toLowerCase() === 'success' - } catch (error) { - console.warn('JWT validation failed:', error) - isValidJwt = false - } - } - - // Only use JWT data if the token is valid - const isRepresentative = isValidJwt ? jwt?.isRepresentative : false - const customerId = isValidJwt ? jwt?.customerId : undefined - const unitId = isValidJwt ? jwt?.unitId : undefined + const params = buildSessionSearchParams( + search, + channel, + postalCode, + country, + geoCoordinates, + oldSession.locale + ) + const marketingData = buildMarketingData(params, oldSession.marketingData) + const { isRepresentative, customerId, unitId } = await resolveJwtClaims( + clients, + headers?.cookie, + account + ) const sessionData = await clients.commerce .session(params.toString()) @@ -130,37 +75,34 @@ export const validateSession = async ( const profile = sessionData?.namespaces.profile ?? null const shopper = sessionData?.namespaces.shopper ?? null - const store = sessionData?.namespaces.store ?? null + const store = (sessionData?.namespaces.store ?? + null) as SessionStoreNamespace | null const authentication = sessionData?.namespaces.authentication ?? null - const checkout = sessionData?.namespaces.checkout ?? null + const checkout = (sessionData?.namespaces.checkout ?? + null) as SessionCheckoutNamespace | null const publicData = sessionData?.namespaces.public ?? null - // Fetch contract data for B2B representatives let contract = null if (isRepresentative && profile?.id?.value) { try { contract = await clients.commerce.masterData.getContractById({ contractId: profile.id.value, }) - } catch (err) { + } catch { console.error( `Error while getting contract data for profile ID (${profile.id.value}).\n` ) } } - // Set seller only if it's inside a region - let seller - if (!!channel.seller && (postalCode || geoCoordinates)) { - const regionData = await clients.commerce.checkout.region({ - postalCode, - geoCoordinates, - country, - salesChannel, - }) - const region = regionData?.[0] - seller = region?.sellers.find((seller) => channel.seller === seller.id) - } + const sellerId = await resolveSellerInRegion( + clients, + channel, + postalCode, + geoCoordinates, + country, + params.get('sc') ?? channel.salesChannel + ) const newSession = { ...oldSession, @@ -169,49 +111,30 @@ export const validateSession = async ( symbol: store?.currencySymbol?.value ?? oldSession.currency.symbol, }, country: store?.countryCode?.value ?? country, - channel: ChannelMarshal.stringify({ - salesChannel: store?.channel?.value ?? channel.salesChannel, - regionId: checkout?.regionId?.value ?? channel.regionId, - seller: seller?.id, - hasOnlyDefaultSalesChannel: !store?.channel?.value, - }), + channel: channelAfterSessionManager( + channel, + store?.channel?.value, + checkout?.regionId?.value, + sellerId + ), /** * B2B data structure in Session: * - Logged user data (shopper): `shopper` namespace * - Unit data: `authentication` namespace * - Contract data: `profile` namespace (those info will be available inside Faststore's Session `person` object) */ - b2b: isRepresentative - ? { - isRepresentative: isRepresentative ?? false, - customerId: authentication?.customerId?.value ?? customerId ?? '', - unitName: authentication?.unitName?.value ?? '', - unitId: authentication?.unitId?.value ?? unitId ?? '', - firstName: - typeof shopper?.firstName?.value === 'string' - ? shopper.firstName.value - : '', - lastName: - typeof shopper?.lastName?.value === 'string' - ? shopper.lastName.value - : '', - userName: - `${typeof shopper?.firstName?.value === 'string' ? shopper.firstName.value : ''} ${typeof shopper?.lastName?.value === 'string' ? shopper.lastName.value : ''}`.trim(), - userEmail: authentication?.storeUserEmail.value ?? '', - savedPostalCode: publicData?.postalCode?.value ?? '', - contractName: resolveActiveContractDisplayName(contract, profile), - organizationManager: shopper?.organizationManager?.value ?? false, - } - : null, + b2b: buildB2bSession({ + isRepresentative, + authentication, + shopper, + publicData, + profile, + contract, + customerId, + unitId, + }), marketingData, - person: profile?.id - ? { - id: profile.id?.value ?? '', - email: profile.email?.value ?? '', - givenName: profile.firstName?.value ?? '', - familyName: profile.lastName?.value ?? '', - } - : null, + person: buildPersonFromProfile(profile), geoCoordinates: (geoCoordinates?.latitude && geoCoordinates?.longitude && diff --git a/packages/api/src/platforms/vtex/typeDefs/order.graphql b/packages/api/src/platforms/vtex/typeDefs/order.graphql index 88d29520c1..925fe20be8 100644 --- a/packages/api/src/platforms/vtex/typeDefs/order.graphql +++ b/packages/api/src/platforms/vtex/typeDefs/order.graphql @@ -14,6 +14,12 @@ type StoreOrder { Indicates whether or not items with attachments should be split. """ shouldSplitItem: Boolean + """ + Sales channel of the underlying orderForm when FastStore adopted it because + the browser session was stale (e.g. after Quick Order). Clients should align + `session.channel` to this value. Null when no SC adoption happened. + """ + salesChannel: String } """ diff --git a/packages/api/src/platforms/vtex/utils/cartSalesChannel.ts b/packages/api/src/platforms/vtex/utils/cartSalesChannel.ts new file mode 100644 index 0000000000..c6567a153e --- /dev/null +++ b/packages/api/src/platforms/vtex/utils/cartSalesChannel.ts @@ -0,0 +1,31 @@ +import type { Channel } from './channel' +import ChannelMarshal from './channel' + +/** + * When session SC and orderForm SC diverge, keep Checkout on the orderForm + * trade policy. Used after external cart changes (stale etag / Quick Order) and + * on later validations while the browser session still lags. + * + * Forcing the session SC (refetch / item updates with `sc=session`) would drop + * items that exist only on the orderForm's sales channel. + */ +export function channelWhenSessionDivergesFromOrderForm( + currentChannel: Required, + orderFormSalesChannel: string | null | undefined +): string | null { + if (orderFormSalesChannel == null || orderFormSalesChannel === '') { + return null + } + + if ( + String(orderFormSalesChannel) === String(currentChannel.salesChannel ?? '') + ) { + return null + } + + return ChannelMarshal.stringify({ + ...currentChannel, + salesChannel: String(orderFormSalesChannel), + hasOnlyDefaultSalesChannel: false, + }) +} diff --git a/packages/api/src/platforms/vtex/utils/sessionChannel.ts b/packages/api/src/platforms/vtex/utils/sessionChannel.ts new file mode 100644 index 0000000000..eaae4d9d94 --- /dev/null +++ b/packages/api/src/platforms/vtex/utils/sessionChannel.ts @@ -0,0 +1,32 @@ +import type { Channel } from './channel' +import ChannelMarshal from './channel' + +/** + * Builds the session `channel` string after Session Manager responds. + * + * When the client already pinned an explicit SC (e.g. adopted from the + * orderForm after Quick Order), keep it. Session Manager often still reports + * the default SC and would otherwise overwrite the adoption. + */ +export function channelAfterSessionManager( + channel: Required, + storeChannelValue: string | null | undefined, + regionId: string | null | undefined, + sellerId: string | undefined +): string { + const hasExplicitSalesChannel = channel.hasOnlyDefaultSalesChannel === false + + const storeSalesChannel = storeChannelValue ?? undefined + const resolvedRegionId = regionId ?? channel.regionId + + return ChannelMarshal.stringify({ + salesChannel: hasExplicitSalesChannel + ? channel.salesChannel || storeSalesChannel + : (storeSalesChannel ?? channel.salesChannel), + regionId: resolvedRegionId ?? undefined, + seller: sellerId, + hasOnlyDefaultSalesChannel: hasExplicitSalesChannel + ? false + : !storeSalesChannel, + }) +} diff --git a/packages/api/src/platforms/vtex/utils/validateSessionHelpers.ts b/packages/api/src/platforms/vtex/utils/validateSessionHelpers.ts new file mode 100644 index 0000000000..eaa9995da8 --- /dev/null +++ b/packages/api/src/platforms/vtex/utils/validateSessionHelpers.ts @@ -0,0 +1,202 @@ +import type { GraphqlContext } from '..' +import type { + StoreMarketingData, + StoreSession, +} from '../../../__generated__/schema' +import type { Channel } from './channel' +import { resolveActiveContractDisplayName } from './contract' +import { getAuthCookie, parseJwt } from './cookies' + +export async function getPreciseLocationData( + clients: GraphqlContext['clients'], + country: string, + postalCode: string +) { + try { + const address = await clients.commerce.checkout.address({ + postalCode, + country, + }) + + const geoCoordinates = address.geoCoordinates + ? { + latitude: address.geoCoordinates[1], + longitude: address.geoCoordinates[0], + } + : null + + return { city: address.city, geoCoordinates } + } catch (err) { + console.error( + `Error while getting geo coordinates for the current postal code (${postalCode}) and country (${country}).\n` + ) + + throw err + } +} + +export function buildSessionSearchParams( + search: string, + channel: Required, + postalCode: string, + country: string, + geoCoordinates: StoreSession['geoCoordinates'], + locale: string +) { + const params = new URLSearchParams(search) + + // Remove facets so they do not interfere with session data / vtex_segment + if (params.has('facets')) { + params.delete('facets') + } + + params.set('sc', params.get('sc') ?? channel.salesChannel) + + if (postalCode) { + params.set('postalCode', postalCode) + } + + if (country) { + params.set('country', country) + } + + if (geoCoordinates) { + params.set( + 'geoCoordinates', + `${geoCoordinates.longitude},${geoCoordinates.latitude}` // long,lat is the format expected + ) + } + + // Sending the locale to the session, the store-session app will update cultureInfo + params.set('locale', locale) + + return params +} + +export function buildMarketingData( + params: URLSearchParams, + oldMarketingData: StoreSession['marketingData'] +): StoreMarketingData { + return { + utmCampaign: + params.get('utm_campaign') ?? oldMarketingData?.utmCampaign ?? '', + utmMedium: params.get('utm_medium') ?? oldMarketingData?.utmMedium ?? '', + utmSource: params.get('utm_source') ?? oldMarketingData?.utmSource ?? '', + utmiCampaign: params.get('utmi_cp') ?? oldMarketingData?.utmiCampaign ?? '', + utmiPage: params.get('utmi_p') ?? oldMarketingData?.utmiPage ?? '', + utmiPart: params.get('utmi_pc') ?? oldMarketingData?.utmiPart ?? '', + } +} + +export async function resolveJwtClaims( + clients: GraphqlContext['clients'], + cookie: string | undefined, + account: string +) { + const jwt = parseJwt(getAuthCookie(cookie ?? '', account)) + let isValidJwt = false + + if (jwt) { + try { + const vtexIdResponse = await clients.commerce.vtexid.validate() + isValidJwt = vtexIdResponse?.authStatus?.toLowerCase() === 'success' + } catch (error) { + console.warn('JWT validation failed:', error) + isValidJwt = false + } + } + + return { + isRepresentative: isValidJwt ? Boolean(jwt?.isRepresentative) : false, + customerId: isValidJwt ? jwt?.customerId : undefined, + unitId: isValidJwt ? jwt?.unitId : undefined, + } +} + +export async function resolveSellerInRegion( + clients: GraphqlContext['clients'], + channel: Required, + postalCode: string, + geoCoordinates: StoreSession['geoCoordinates'], + country: string, + salesChannel: string +) { + if (!channel.seller || !(postalCode || geoCoordinates)) { + return undefined + } + + const regionData = await clients.commerce.checkout.region({ + postalCode, + geoCoordinates, + country, + salesChannel, + }) + const region = regionData?.[0] + + return region?.sellers.find((seller) => channel.seller === seller.id)?.id +} + +function shopperString(value: { value?: unknown } | null | undefined): string { + return typeof value?.value === 'string' ? value.value : '' +} + +export function buildB2bSession(args: { + isRepresentative: boolean + authentication: { + customerId?: { value?: string | null } | null + unitName?: { value?: string | null } | null + unitId?: { value?: string | null } | null + storeUserEmail?: { value?: string | null } | null + } | null + shopper: { + firstName?: { value?: unknown } | null + lastName?: { value?: unknown } | null + organizationManager?: { value?: boolean | null } | null + } | null + publicData: { postalCode?: { value?: string | null } | null } | null + profile: Parameters[1] + contract: Parameters[0] + customerId?: string + unitId?: string +}) { + if (!args.isRepresentative) { + return null + } + + const firstName = shopperString(args.shopper?.firstName) + const lastName = shopperString(args.shopper?.lastName) + + return { + isRepresentative: true, + customerId: args.authentication?.customerId?.value ?? args.customerId ?? '', + unitName: args.authentication?.unitName?.value ?? '', + unitId: args.authentication?.unitId?.value ?? args.unitId ?? '', + firstName, + lastName, + userName: `${firstName} ${lastName}`.trim(), + userEmail: args.authentication?.storeUserEmail?.value ?? '', + savedPostalCode: args.publicData?.postalCode?.value ?? '', + contractName: resolveActiveContractDisplayName(args.contract, args.profile), + organizationManager: args.shopper?.organizationManager?.value ?? false, + } +} + +export function buildPersonFromProfile( + profile: { + id?: { value?: string | null } | null + email?: { value?: string | null } | null + firstName?: { value?: string | null } | null + lastName?: { value?: string | null } | null + } | null +) { + if (!profile?.id) { + return null + } + + return { + id: profile.id?.value ?? '', + email: profile.email?.value ?? '', + givenName: profile.firstName?.value ?? '', + familyName: profile.lastName?.value ?? '', + } +} diff --git a/packages/api/src/typings/globals.ts b/packages/api/src/typings/globals.ts index 6d27009546..1a0bf0777c 100644 --- a/packages/api/src/typings/globals.ts +++ b/packages/api/src/typings/globals.ts @@ -18,12 +18,9 @@ interface Options { incrementAddress: boolean flags?: FeatureFlags version?: string - OTEL?: { - enabled: boolean - traceparent?: string - tracestate?: string - } discoveryConfig?: Record + /** Emit OpenTelemetry spans from resolvers. Defaults to disabled. */ + OTEL_ENABLED?: boolean } interface FeatureFlags { diff --git a/packages/api/test/integration/__snapshots__/mutations.test.ts.snap b/packages/api/test/integration/__snapshots__/mutations.test.ts.snap index d1457e4b00..2a54eed993 100644 --- a/packages/api/test/integration/__snapshots__/mutations.test.ts.snap +++ b/packages/api/test/integration/__snapshots__/mutations.test.ts.snap @@ -24,6 +24,7 @@ exports[`\`validateCart\` mutation should return new cart when etag is stale 1`] }, ], "orderNumber": "edbe3b03c8c94827a37ec5a6a4648fd2", + "salesChannel": null, }, }, }, @@ -54,6 +55,7 @@ exports[`\`validateCart\` mutation should return the full order when an invalid }, ], "orderNumber": "edbe3b03c8c94827a37ec5a6a4648fd2", + "salesChannel": null, }, }, }, diff --git a/packages/api/test/integration/mutations.test.ts b/packages/api/test/integration/mutations.test.ts index 13aa30cf07..9878c4284e 100644 --- a/packages/api/test/integration/mutations.test.ts +++ b/packages/api/test/integration/mutations.test.ts @@ -7,6 +7,7 @@ import { ValidateCartMutation, checkoutOrderFormCustomDataInvalidFetch, checkoutOrderFormCustomDataStaleFetch, + checkoutOrderFormCustomDataValidFetch, checkoutOrderFormInvalidFetch, checkoutOrderFormItemsInvalidFetch, checkoutOrderFormStaleFetch, @@ -24,6 +25,7 @@ const apiOptions = { locale: 'en-US', subDomainPrefix: ['www'], hideUnavailableItems: false, + showSponsored: false, incrementAddress: false, flags: { enableOrderFormSync: true, @@ -164,3 +166,134 @@ test('`validateCart` mutation should return new cart when etag is stale', async expect(response).toMatchSnapshot() }) + +const withOrderFormSalesChannel = < + T extends { result: { salesChannel?: string } }, +>( + fetchMock: T, + salesChannel: string +) => ({ + ...fetchMock, + result: { + ...fetchMock.result, + salesChannel, + }, +}) + +const salesChannelFetch = (salesChannel: string) => ({ + ...salesChannelStaleFetch, + info: salesChannelStaleFetch.info.replace( + '/saleschannel/1', + `/saleschannel/${salesChannel}` + ), + result: { + ...salesChannelStaleFetch.result, + Id: Number(salesChannel), + }, +}) + +/** + * Non-stale orderForm (valid etag) + divergent browser cart → item update path. + * Session channel remains SC1 via apiOptions; orderForm is on SC2. + */ +test('`validateCart` updates items with orderForm SC when session SC diverges', async () => { + const run = await createRunner() + const orderFormOnSc2 = withOrderFormSalesChannel( + checkoutOrderFormValidFetch, + '2' + ) + const customDataOnSc2 = withOrderFormSalesChannel( + checkoutOrderFormCustomDataValidFetch, + '2' + ) + + mockedFetch.mockImplementation((info, init, options) => { + const url = String(info) + + if (url.includes('/items?')) { + expect(url).toContain('sc=2') + expect(url).not.toContain('sc=1') + return { + ...checkoutOrderFormItemsInvalidFetch.result, + salesChannel: '2', + } + } + + if (url.includes('/customData/faststore/cartEtag')) { + return customDataOnSc2.result + } + + return pickFetchAPICallResult(info, init, [ + orderFormOnSc2, + productSearchPage1Count1Fetch, + salesChannelFetch('2'), + ]) + }) + + const response = await run(ValidateCartMutation, { cart: InvalidCart }) + + const getOrderFormUrl = mockedFetch.mock.calls + .map(([info]) => String(info)) + .find( + (url) => + /\/orderForm\/[^/?]+(\?|$)/.test(url) && + !url.includes('/items') && + !url.includes('/customData') + ) + const itemsUrl = mockedFetch.mock.calls + .map(([info]) => String(info)) + .find((url) => url.includes('/items?')) + + expect(getOrderFormUrl).toBeDefined() + expect(getOrderFormUrl).not.toContain('sc=') + expect(itemsUrl).toBeDefined() + expect(itemsUrl).toContain('sc=2') + expect( + mockedFetch.mock.calls.some(([info]) => String(info).includes('sc=1')) + ).toBe(false) + expect(response.errors).toBeUndefined() + expect(response.data?.validateCart).not.toBeNull() + expect(response.data?.validateCart?.order?.salesChannel).toBe('2') +}) + +test('`validateCart` adopts orderForm SC on stale etag without refetching session SC', async () => { + const run = await createRunner() + const staleOnSc2 = withOrderFormSalesChannel(checkoutOrderFormStaleFetch, '2') + const customDataOnSc2 = withOrderFormSalesChannel( + checkoutOrderFormCustomDataStaleFetch, + '2' + ) + + mockedFetch.mockImplementation((info, init) => + pickFetchAPICallResult(info, init, [ + staleOnSc2, + customDataOnSc2, + productSearchPage1Count1Fetch, + salesChannelFetch('2'), + ]) + ) + + const response = await run(ValidateCartMutation, { cart: InvalidCart }) + + const checkoutUrls = mockedFetch.mock.calls + .map(([info]) => String(info)) + .filter((url) => url.includes('/api/checkout/pub/orderForm')) + const getOrderFormUrl = checkoutUrls.find( + (url) => + /\/orderForm\/[^/?]+(\?|$)/.test(url) && + !url.includes('/items') && + !url.includes('/customData') + ) + + expect(getOrderFormUrl).toBeDefined() + expect(getOrderFormUrl).not.toContain('sc=') + expect(checkoutUrls.some((url) => url.includes('sc=1'))).toBe(false) + expect( + mockedFetch.mock.calls.some(([info]) => + String(info).includes('/saleschannel/2') + ) + ).toBe(true) + expect(response.errors).toBeUndefined() + expect(response.data?.validateCart).not.toBeNull() + expect(response.data?.validateCart?.order?.salesChannel).toBe('2') +}) diff --git a/packages/api/test/mocks/ValidateCartMutation.ts b/packages/api/test/mocks/ValidateCartMutation.ts index e5620945fb..3a54fe2e70 100644 --- a/packages/api/test/mocks/ValidateCartMutation.ts +++ b/packages/api/test/mocks/ValidateCartMutation.ts @@ -7,6 +7,7 @@ export const ValidateCartMutation = `mutation ValidateCartMutation($cart: IStore } order { orderNumber + salesChannel acceptedOffer { availability itemCondition @@ -262,7 +263,7 @@ export const InvalidCart = { // Valid Cart export const checkoutOrderFormValidFetch = { - info: 'https://storeframework.vtexcommercestable.com.br/api/checkout/pub/orderForm/edbe3b03c8c94827a37ec5a6a4648fd2?sc=1&refreshOutdatedData=true', + info: 'https://storeframework.vtexcommercestable.com.br/api/checkout/pub/orderForm/edbe3b03c8c94827a37ec5a6a4648fd2?refreshOutdatedData=true', init: { method: 'POST', headers: { 'content-type': 'application/json', 'X-FORWARDED-HOST': '' }, @@ -289,7 +290,7 @@ export const checkoutOrderFormCustomDataValidFetch = { // "Invalid" Cart export const checkoutOrderFormInvalidFetch = { - info: 'https://storeframework.vtexcommercestable.com.br/api/checkout/pub/orderForm/edbe3b03c8c94827a37ec5a6a4648fd2?sc=1&refreshOutdatedData=true', + info: 'https://storeframework.vtexcommercestable.com.br/api/checkout/pub/orderForm/edbe3b03c8c94827a37ec5a6a4648fd2?refreshOutdatedData=true', init: { method: 'POST', headers: { 'content-type': 'application/json', 'X-FORWARDED-HOST': '' }, @@ -515,7 +516,7 @@ export const createProductFetchResultForSku = ( // Stale Cart export const checkoutOrderFormStaleFetch = { - info: 'https://storeframework.vtexcommercestable.com.br/api/checkout/pub/orderForm/edbe3b03c8c94827a37ec5a6a4648fd2?sc=1&refreshOutdatedData=true', + info: 'https://storeframework.vtexcommercestable.com.br/api/checkout/pub/orderForm/edbe3b03c8c94827a37ec5a6a4648fd2?refreshOutdatedData=true', init: { method: 'POST', headers: { 'content-type': 'application/json', 'X-FORWARDED-HOST': '' }, diff --git a/packages/api/test/unit/observability/telemetry.test.ts b/packages/api/test/unit/observability/telemetry.test.ts new file mode 100644 index 0000000000..4872ba9ac8 --- /dev/null +++ b/packages/api/test/unit/observability/telemetry.test.ts @@ -0,0 +1,196 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { name, version } from '../../../package.json' +import { ResolverTrace } from '../../../src/observability/telemetry' + +const otel = vi.hoisted(() => { + const span = { + end: vi.fn(), + setStatus: vi.fn(), + recordException: vi.fn(), + } + const startSpan = vi.fn(() => span) + + return { + span, + startSpan, + log: vi.fn(), + getTracer: vi.fn(() => ({ startSpan })), + setSpan: vi.fn((context: unknown, activeSpan: unknown) => ({ + context, + activeSpan, + })), + active: vi.fn(() => 'root-context'), + with: vi.fn((_context: unknown, fn: () => unknown) => fn()), + } +}) + +vi.mock('@faststore/diagnostics', () => ({ + logger: () => otel.log, + OTELAPI: { + SpanKind: { INTERNAL: 'INTERNAL' }, + SpanStatusCode: { ERROR: 'ERROR' }, + trace: { + getTracer: otel.getTracer, + setSpan: otel.setSpan, + }, + context: { + active: otel.active, + with: otel.with, + }, + }, +})) + +const makeContext = (OTEL_ENABLED: boolean) => ({ + OTEL_ENABLED, + account: 'faststoretest', + discoveryConfig: {}, +}) + +const resolverInfo = { + parentType: { name: 'StoreProduct' }, + fieldName: 'offers', +} + +let consoleError: ReturnType + +beforeEach(() => { + vi.clearAllMocks() + consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) +}) + +afterEach(() => { + consoleError.mockRestore() +}) + +describe('ResolverTrace when telemetry is disabled', () => { + it('calls the resolver directly, forwarding every argument', () => { + const resolver = vi.fn(() => 'result') + const context = makeContext(false) + + const traced = ResolverTrace(resolver, 'Query(product)') + + expect(traced('source', { id: '1' }, context, resolverInfo)).toBe('result') + expect(resolver).toHaveBeenCalledWith( + 'source', + { id: '1' }, + context, + resolverInfo + ) + expect(otel.startSpan).not.toHaveBeenCalled() + }) + + it('calls the resolver directly when there is no graphql context', () => { + const resolver = vi.fn(() => 'result') + + const traced = ResolverTrace(resolver, 'Query(product)') + + expect(traced('source', {}, undefined as never, resolverInfo)).toBe( + 'result' + ) + expect(otel.startSpan).not.toHaveBeenCalled() + }) +}) + +describe('ResolverTrace when telemetry is enabled', () => { + it('opens a span carrying the resolver and package metadata', () => { + const traced = ResolverTrace(() => 'result', 'Query(product)') + + traced('source', {}, makeContext(true), resolverInfo) + + expect(otel.startSpan).toHaveBeenCalledWith('Query(product)', { + kind: 'INTERNAL', + attributes: { + timestamp: expect.any(Number), + '@faststore_version': version, + '@faststore_package_name': name, + '@faststore_account_name': 'faststoretest', + '@faststore_environment': process.env.NODE_ENV, + '@faststore_resolver_parent_type': 'StoreProduct', + '@faststore_resolver_field_name': 'offers', + }, + }) + }) + + it('falls back to a placeholder name when the resolver is unnamed', () => { + const traced = ResolverTrace(() => 'result') + + traced('source', {}, makeContext(true), undefined) + + expect(otel.startSpan).toHaveBeenCalledWith( + 'Unknown Graphql Resolver', + expect.objectContaining({ + attributes: expect.objectContaining({ + '@faststore_resolver_parent_type': undefined, + '@faststore_resolver_field_name': undefined, + }), + }) + ) + }) + + it('runs a synchronous resolver inside the span context and ends the span', () => { + const resolver = vi.fn(() => 'result') + + const traced = ResolverTrace(resolver, 'Query(product)') + + expect(traced('source', {}, makeContext(true), resolverInfo)).toBe('result') + expect(otel.setSpan).toHaveBeenCalledWith('root-context', otel.span) + expect(otel.with).toHaveBeenCalledWith( + { context: 'root-context', activeSpan: otel.span }, + expect.any(Function) + ) + expect(otel.span.end).toHaveBeenCalledTimes(1) + }) + + it('keeps the span open until an async resolver settles', async () => { + const traced = ResolverTrace( + () => Promise.resolve('result'), + 'Query(product)' + ) + + const pending = traced('source', {}, makeContext(true), resolverInfo) + expect(otel.span.end).not.toHaveBeenCalled() + + await expect(pending).resolves.toBe('result') + expect(otel.span.end).toHaveBeenCalledTimes(1) + expect(otel.span.setStatus).not.toHaveBeenCalled() + }) + + it('records and rethrows a rejected async resolver', async () => { + const error = new Error('upstream is down') + const traced = ResolverTrace(() => Promise.reject(error), 'Query(product)') + + await expect( + traced('source', {}, makeContext(true), resolverInfo) + ).rejects.toThrow(error) + + expect(otel.span.setStatus).toHaveBeenCalledWith({ code: 'ERROR' }) + expect(otel.span.recordException).toHaveBeenCalledWith(error) + expect(otel.span.end).toHaveBeenCalledTimes(1) + expect(consoleError).toHaveBeenCalledWith( + 'Error at resolver: Query(product): \n %o', + error + ) + expect(otel.log).toHaveBeenCalledWith( + 'error', + 'Error at resolver: %s\n%o', + 'Query(product)', + error + ) + }) + + it('records and rethrows a synchronous resolver failure', () => { + const error = new Error('boom') + const traced = ResolverTrace(() => { + throw error + }, 'Query(product)') + + expect(() => traced('source', {}, makeContext(true), resolverInfo)).toThrow( + error + ) + + expect(otel.span.setStatus).toHaveBeenCalledWith({ code: 'ERROR' }) + expect(otel.span.recordException).toHaveBeenCalledWith(error) + expect(otel.span.end).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/api/test/unit/platforms/vtex/clients/commerce.test.ts b/packages/api/test/unit/platforms/vtex/clients/commerce/index.test.ts similarity index 90% rename from packages/api/test/unit/platforms/vtex/clients/commerce.test.ts rename to packages/api/test/unit/platforms/vtex/clients/commerce/index.test.ts index 127d93290c..70945efb08 100644 --- a/packages/api/test/unit/platforms/vtex/clients/commerce.test.ts +++ b/packages/api/test/unit/platforms/vtex/clients/commerce/index.test.ts @@ -1,8 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import * as clients from '../../../../../src/platforms/vtex/clients' -// This should be imported AFTER the '../../../../../src/platforms/vtex/clients' -import { NotFoundError } from '../../../../../src/platforms/errors' -import { GraphqlVtexContextFactory } from '../../../../../src/platforms/vtex' +import * as clients from '../../../../../../src/platforms/vtex/clients' +// This should be imported AFTER the '../../../../../../src/platforms/vtex/clients' +import { NotFoundError } from '../../../../../../src/platforms/errors' +import { GraphqlVtexContextFactory } from '../../../../../../src/platforms/vtex' const apiOptions = { platform: 'vtex', @@ -27,7 +27,7 @@ beforeEach(() => { fetchAPIMocked.mockClear() }) -vi.mock('../../../../../src/platforms/vtex/clients/fetch.ts', () => ({ +vi.mock('../../../../../../src/platforms/vtex/clients/fetch.ts', () => ({ fetchAPI: async ( info: RequestInfo, init?: RequestInit, @@ -71,6 +71,61 @@ describe('VTEX Commerce', () => { expect(fetchAPIMocked).not.toHaveBeenCalled() }) }) + + describe('orderForm', () => { + it('includes sc query param by default', async () => { + fetchAPIMocked.mockResolvedValueOnce({ + orderFormId: 'of-1', + salesChannel: '1', + items: [], + }) + + const { commerce } = clients.getClients(apiOptions, context) + await commerce.checkout.orderForm({ id: 'of-1' }) + + const [url] = fetchAPIMocked.mock.calls[0] + expect(url).toContain('/api/checkout/pub/orderForm/of-1?') + expect(url).toContain('sc=1') + expect(url).toContain('refreshOutdatedData=true') + }) + + it('omits sc when preserveSalesChannel is true for an existing cart', async () => { + fetchAPIMocked.mockResolvedValueOnce({ + orderFormId: 'of-1', + salesChannel: '4', + items: [{ id: 'sku-1' }], + }) + + const { commerce } = clients.getClients(apiOptions, context) + await commerce.checkout.orderForm({ + id: 'of-1', + preserveSalesChannel: true, + }) + + const [url] = fetchAPIMocked.mock.calls[0] + expect(url).toContain('/api/checkout/pub/orderForm/of-1?') + expect(url).not.toContain('sc=') + expect(url).toContain('refreshOutdatedData=true') + }) + + it('still sends sc when preserveSalesChannel is true but creating a new cart', async () => { + fetchAPIMocked.mockResolvedValueOnce({ + orderFormId: 'of-new', + salesChannel: '1', + items: [], + }) + + const { commerce } = clients.getClients(apiOptions, context) + await commerce.checkout.orderForm({ + preserveSalesChannel: true, + }) + + const [url] = fetchAPIMocked.mock.calls[0] + expect(url).toContain('/api/checkout/pub/orderForm?') + expect(url).toContain('sc=1') + expect(url).not.toContain('refreshOutdatedData') + }) + }) }) describe('Order Entry', () => { diff --git a/packages/api/test/unit/platforms/vtex/resolvers/validateSession.test.ts b/packages/api/test/unit/platforms/vtex/resolvers/validateSession.test.ts new file mode 100644 index 0000000000..a13276e57a --- /dev/null +++ b/packages/api/test/unit/platforms/vtex/resolvers/validateSession.test.ts @@ -0,0 +1,222 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { validateSession } from '../../../../../src/platforms/vtex/resolvers/validateSession' +import ChannelMarshal from '../../../../../src/platforms/vtex/utils/channel' + +const baseSession = { + locale: 'pt-BR', + currency: { code: 'BRL', symbol: 'R$' }, + country: 'BRA', + channel: ChannelMarshal.stringify({ + salesChannel: '1', + regionId: '', + seller: '', + hasOnlyDefaultSalesChannel: true, + }), + deliveryMode: null, + addressType: null, + city: 'São Paulo', + postalCode: null, + geoCoordinates: { latitude: -23.5, longitude: -46.6 }, + person: null, + b2b: null, + marketingData: { + utmCampaign: '', + utmMedium: '', + utmSource: '', + utmiCampaign: '', + utmiPage: '', + utmiPart: '', + }, + refreshAfter: null, +} + +const makeContext = (sessionNamespaces: Record = {}) => { + const session = vi.fn().mockResolvedValue({ + namespaces: { + store: { + channel: { value: '1' }, + currencyCode: { value: 'BRL' }, + currencySymbol: { value: 'R$' }, + countryCode: { value: 'BRA' }, + }, + checkout: { regionId: { value: '' } }, + profile: null, + shopper: null, + authentication: null, + public: null, + ...sessionNamespaces, + }, + }) + + return { + clients: { + commerce: { + session, + checkout: { + address: vi.fn(), + region: vi.fn(), + }, + vtexid: { validate: vi.fn() }, + masterData: { getContractById: vi.fn() }, + }, + }, + headers: { cookie: '' }, + account: 'sabrinastore', + } as any +} + +describe('validateSession', () => { + beforeEach(() => { + vi.restoreAllMocks() + }) + + it('keeps an explicit client sales channel when Session Manager diverges', async () => { + const ctx = makeContext({ + store: { + channel: { value: '1' }, + currencyCode: { value: 'BRL' }, + currencySymbol: { value: 'R$' }, + countryCode: { value: 'BRA' }, + }, + }) + const oldSession = { + ...baseSession, + channel: ChannelMarshal.stringify({ + salesChannel: '2', + regionId: '', + seller: '', + hasOnlyDefaultSalesChannel: false, + }), + } + + const result = await validateSession( + null, + { session: oldSession, search: '' }, + ctx + ) + + expect(result).not.toBeNull() + expect(JSON.parse(result!.channel!)).toMatchObject({ + salesChannel: '2', + hasOnlyDefaultSalesChannel: false, + }) + }) + + it('adopts Session Manager sales channel for the default client channel', async () => { + const ctx = makeContext({ + store: { + channel: { value: '4' }, + currencyCode: { value: 'BRL' }, + currencySymbol: { value: 'R$' }, + countryCode: { value: 'BRA' }, + }, + }) + + const result = await validateSession( + null, + { session: baseSession, search: '' }, + ctx + ) + + expect(JSON.parse(result!.channel!)).toMatchObject({ + salesChannel: '4', + hasOnlyDefaultSalesChannel: false, + }) + }) + + it('survives Session Manager failures', async () => { + const ctx = makeContext() + ctx.clients.commerce.session.mockRejectedValue(new Error('session down')) + + const result = await validateSession( + null, + { session: baseSession, search: '' }, + ctx + ) + + expect(result).not.toBeNull() + expect(JSON.parse(result!.channel!).salesChannel).toBe('1') + }) + + it('loads precise location when city/geo are missing', async () => { + const ctx = makeContext() + ctx.clients.commerce.checkout.address.mockResolvedValue({ + city: 'Curitiba', + geoCoordinates: [-49.2, -25.4], + }) + + const result = await validateSession( + null, + { + session: { + ...baseSession, + city: null, + geoCoordinates: null, + postalCode: '80010-000', + }, + search: '', + }, + ctx + ) + + expect(ctx.clients.commerce.checkout.address).toHaveBeenCalled() + expect(result?.city).toBe('Curitiba') + expect(result?.geoCoordinates).toEqual({ + latitude: -25.4, + longitude: -49.2, + }) + }) + + it('maps profile into person when Session Manager returns one', async () => { + const ctx = makeContext({ + profile: { + id: { value: 'p1' }, + email: { value: 'a@b.com' }, + firstName: { value: 'Ada' }, + lastName: { value: 'Lovelace' }, + }, + }) + + const result = await validateSession( + null, + { session: baseSession, search: '' }, + ctx + ) + + expect(result?.person).toEqual({ + id: 'p1', + email: 'a@b.com', + givenName: 'Ada', + familyName: 'Lovelace', + }) + }) + + it('resolves a seller when channel has one and postal code is set', async () => { + const ctx = makeContext() + ctx.clients.commerce.checkout.region.mockResolvedValue([ + { sellers: [{ id: 'seller-a' }] }, + ]) + + const result = await validateSession( + null, + { + session: { + ...baseSession, + postalCode: '01310-100', + channel: ChannelMarshal.stringify({ + salesChannel: '1', + regionId: '', + seller: 'seller-a', + hasOnlyDefaultSalesChannel: true, + }), + }, + search: '', + }, + ctx + ) + + expect(ctx.clients.commerce.checkout.region).toHaveBeenCalled() + expect(JSON.parse(result!.channel!).seller).toBe('seller-a') + }) +}) diff --git a/packages/api/test/unit/platforms/vtex/utils/cartSalesChannel.test.ts b/packages/api/test/unit/platforms/vtex/utils/cartSalesChannel.test.ts new file mode 100644 index 0000000000..f8900e8768 --- /dev/null +++ b/packages/api/test/unit/platforms/vtex/utils/cartSalesChannel.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest' +import { channelWhenSessionDivergesFromOrderForm } from '../../../../../src/platforms/vtex/utils/cartSalesChannel' +import type { Channel } from '../../../../../src/platforms/vtex/utils/channel' + +const baseChannel = (salesChannel: string): Required => ({ + salesChannel, + regionId: '', + seller: '', + hasOnlyDefaultSalesChannel: true, +}) + +describe('channelWhenSessionDivergesFromOrderForm', () => { + it('adopts orderForm SC when session lags behind the cart', () => { + const result = channelWhenSessionDivergesFromOrderForm( + baseChannel('1'), + '2' + ) + + if (result === null) { + throw new Error('expected channel adoption when session diverges') + } + + expect(JSON.parse(result)).toMatchObject({ + salesChannel: '2', + hasOnlyDefaultSalesChannel: false, + }) + }) + + it('returns null when SCs already match', () => { + expect( + channelWhenSessionDivergesFromOrderForm(baseChannel('2'), '2') + ).toBeNull() + }) + + it('returns null when orderForm SC is missing', () => { + expect( + channelWhenSessionDivergesFromOrderForm(baseChannel('1'), null) + ).toBeNull() + expect( + channelWhenSessionDivergesFromOrderForm(baseChannel('1'), '') + ).toBeNull() + }) +}) diff --git a/packages/api/test/unit/platforms/vtex/utils/sessionChannel.test.ts b/packages/api/test/unit/platforms/vtex/utils/sessionChannel.test.ts new file mode 100644 index 0000000000..4abbc2dab2 --- /dev/null +++ b/packages/api/test/unit/platforms/vtex/utils/sessionChannel.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest' +import { channelAfterSessionManager } from '../../../../../src/platforms/vtex/utils/sessionChannel' +import type { Channel } from '../../../../../src/platforms/vtex/utils/channel' + +const baseChannel = ( + salesChannel: string, + hasOnlyDefaultSalesChannel = true +): Required => ({ + salesChannel, + regionId: 'r1', + seller: '', + hasOnlyDefaultSalesChannel, +}) + +describe('channelAfterSessionManager', () => { + it('keeps an explicit client SC when Session Manager reports another', () => { + expect( + JSON.parse( + channelAfterSessionManager(baseChannel('2', false), '1', 'r2', 's1') + ) + ).toMatchObject({ + salesChannel: '2', + regionId: 'r2', + seller: 's1', + hasOnlyDefaultSalesChannel: false, + }) + }) + + it('prefers Session Manager SC when the client still has the default', () => { + expect( + JSON.parse( + channelAfterSessionManager(baseChannel('1', true), '4', null, undefined) + ) + ).toMatchObject({ + salesChannel: '4', + regionId: 'r1', + hasOnlyDefaultSalesChannel: false, + }) + }) + + it('falls back to the client SC when Session Manager has none', () => { + expect( + JSON.parse( + channelAfterSessionManager( + baseChannel('1', true), + null, + undefined, + undefined + ) + ) + ).toMatchObject({ + salesChannel: '1', + hasOnlyDefaultSalesChannel: true, + }) + }) +}) diff --git a/packages/api/test/unit/platforms/vtex/utils/validateSessionHelpers.test.ts b/packages/api/test/unit/platforms/vtex/utils/validateSessionHelpers.test.ts new file mode 100644 index 0000000000..ab24a41978 --- /dev/null +++ b/packages/api/test/unit/platforms/vtex/utils/validateSessionHelpers.test.ts @@ -0,0 +1,359 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { Channel } from '../../../../../src/platforms/vtex/utils/channel' + +const getAuthCookie = vi.fn(() => '') +const parseJwt = vi.fn(() => null as any) + +vi.mock('../../../../../src/platforms/vtex/utils/cookies', async () => { + const actual = await vi.importActual< + typeof import('../../../../../src/platforms/vtex/utils/cookies') + >('../../../../../src/platforms/vtex/utils/cookies') + + return { + ...actual, + getAuthCookie: (...args: unknown[]) => getAuthCookie(...args), + parseJwt: (...args: unknown[]) => parseJwt(...args), + } +}) + +const { + buildB2bSession, + buildMarketingData, + buildPersonFromProfile, + buildSessionSearchParams, + getPreciseLocationData, + resolveJwtClaims, + resolveSellerInRegion, +} = await import( + '../../../../../src/platforms/vtex/utils/validateSessionHelpers' +) + +const baseChannel = (salesChannel = '1', seller = ''): Required => ({ + salesChannel, + regionId: '', + seller, + hasOnlyDefaultSalesChannel: true, +}) + +describe('buildSessionSearchParams', () => { + it('sets sc from search or falls back to channel', () => { + const fromSearch = buildSessionSearchParams( + '?sc=4', + baseChannel('1'), + '', + '', + null, + 'pt-BR' + ) + expect(fromSearch.get('sc')).toBe('4') + expect(fromSearch.get('locale')).toBe('pt-BR') + + const fromChannel = buildSessionSearchParams( + '', + baseChannel('2'), + '', + '', + null, + 'en-US' + ) + expect(fromChannel.get('sc')).toBe('2') + }) + + it('drops facets and sets location params', () => { + const params = buildSessionSearchParams( + '?facets=category-1&utm_source=ads', + baseChannel(), + '01310-100', + 'BRA', + { latitude: -23.5, longitude: -46.6 }, + 'pt-BR' + ) + + expect(params.has('facets')).toBe(false) + expect(params.get('postalCode')).toBe('01310-100') + expect(params.get('country')).toBe('BRA') + expect(params.get('geoCoordinates')).toBe('-46.6,-23.5') + expect(params.get('utm_source')).toBe('ads') + }) +}) + +describe('buildMarketingData', () => { + it('prefers query params over the previous session values', () => { + const params = new URLSearchParams( + 'utm_campaign=c&utm_medium=m&utm_source=s&utmi_cp=cp&utmi_p=p&utmi_pc=pc' + ) + + expect( + buildMarketingData(params, { + utmCampaign: 'old', + utmMedium: 'old', + utmSource: 'old', + utmiCampaign: 'old', + utmiPage: 'old', + utmiPart: 'old', + }) + ).toEqual({ + utmCampaign: 'c', + utmMedium: 'm', + utmSource: 's', + utmiCampaign: 'cp', + utmiPage: 'p', + utmiPart: 'pc', + }) + }) + + it('falls back to empty strings when nothing is set', () => { + expect(buildMarketingData(new URLSearchParams(), null)).toEqual({ + utmCampaign: '', + utmMedium: '', + utmSource: '', + utmiCampaign: '', + utmiPage: '', + utmiPart: '', + }) + }) +}) + +describe('buildPersonFromProfile', () => { + it('returns null without a profile id', () => { + expect(buildPersonFromProfile(null)).toBeNull() + expect(buildPersonFromProfile({})).toBeNull() + }) + + it('maps profile fields into a Store person', () => { + expect( + buildPersonFromProfile({ + id: { value: 'p1' }, + email: { value: 'a@b.com' }, + firstName: { value: 'Ada' }, + lastName: { value: 'Lovelace' }, + }) + ).toEqual({ + id: 'p1', + email: 'a@b.com', + givenName: 'Ada', + familyName: 'Lovelace', + }) + }) +}) + +describe('buildB2bSession', () => { + it('returns null when the shopper is not a representative', () => { + expect( + buildB2bSession({ + isRepresentative: false, + authentication: null, + shopper: null, + publicData: null, + profile: null, + contract: null, + }) + ).toBeNull() + }) + + it('builds B2B fields from authentication and shopper namespaces', () => { + expect( + buildB2bSession({ + isRepresentative: true, + authentication: { + customerId: { value: 'cust' }, + unitName: { value: 'Unit' }, + unitId: { value: 'u1' }, + storeUserEmail: { value: 'rep@vtex.com' }, + }, + shopper: { + firstName: { value: 'Jane' }, + lastName: { value: 'Doe' }, + organizationManager: { value: true }, + }, + publicData: { postalCode: { value: '12345' } }, + profile: { id: { value: 'p1' } }, + contract: { corporateName: 'Acme' }, + customerId: 'fallback-cust', + unitId: 'fallback-unit', + }) + ).toMatchObject({ + isRepresentative: true, + customerId: 'cust', + unitName: 'Unit', + unitId: 'u1', + firstName: 'Jane', + lastName: 'Doe', + userName: 'Jane Doe', + userEmail: 'rep@vtex.com', + savedPostalCode: '12345', + contractName: 'Acme', + organizationManager: true, + }) + }) + + it('ignores non-string shopper name values', () => { + const result = buildB2bSession({ + isRepresentative: true, + authentication: null, + shopper: { + firstName: { value: 123 }, + lastName: { value: null }, + }, + publicData: null, + profile: null, + contract: null, + customerId: 'c', + unitId: 'u', + }) + + expect(result).toMatchObject({ + firstName: '', + lastName: '', + userName: '', + customerId: 'c', + unitId: 'u', + }) + }) +}) + +describe('getPreciseLocationData', () => { + it('maps checkout address geoCoordinates', async () => { + const address = vi.fn().mockResolvedValue({ + city: 'São Paulo', + geoCoordinates: [-46.6, -23.5], + }) + const clients = { commerce: { checkout: { address } } } as any + + await expect( + getPreciseLocationData(clients, 'BRA', '01310-100') + ).resolves.toEqual({ + city: 'São Paulo', + geoCoordinates: { latitude: -23.5, longitude: -46.6 }, + }) + }) + + it('returns null geoCoordinates when the address has none', async () => { + const clients = { + commerce: { + checkout: { + address: vi + .fn() + .mockResolvedValue({ city: 'X', geoCoordinates: null }), + }, + }, + } as any + + await expect(getPreciseLocationData(clients, 'BRA', '1')).resolves.toEqual({ + city: 'X', + geoCoordinates: null, + }) + }) + + it('rethrows after logging when address lookup fails', async () => { + const error = new Error('boom') + const clients = { + commerce: { + checkout: { address: vi.fn().mockRejectedValue(error) }, + }, + } as any + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + await expect(getPreciseLocationData(clients, 'BRA', '1')).rejects.toThrow( + 'boom' + ) + expect(spy).toHaveBeenCalled() + spy.mockRestore() + }) +}) + +describe('resolveSellerInRegion', () => { + it('skips the region lookup without a seller or location', async () => { + const region = vi.fn() + const clients = { commerce: { checkout: { region } } } as any + + await expect( + resolveSellerInRegion(clients, baseChannel(), '', null, 'BRA', '1') + ).resolves.toBeUndefined() + expect(region).not.toHaveBeenCalled() + }) + + it('returns the matching seller id from region data', async () => { + const region = vi.fn().mockResolvedValue([ + { + sellers: [{ id: '1' }, { id: 'seller-a' }], + }, + ]) + const clients = { commerce: { checkout: { region } } } as any + + await expect( + resolveSellerInRegion( + clients, + baseChannel('1', 'seller-a'), + '01310-100', + null, + 'BRA', + '1' + ) + ).resolves.toBe('seller-a') + }) +}) + +describe('resolveJwtClaims', () => { + beforeEach(() => { + getAuthCookie.mockReset() + parseJwt.mockReset() + getAuthCookie.mockReturnValue('') + parseJwt.mockReturnValue(null) + }) + + it('returns empty claims when there is no auth cookie', async () => { + const validate = vi.fn() + const clients = { commerce: { vtexid: { validate } } } as any + + await expect( + resolveJwtClaims(clients, undefined, 'storeframework') + ).resolves.toEqual({ + isRepresentative: false, + customerId: undefined, + unitId: undefined, + }) + expect(validate).not.toHaveBeenCalled() + }) + + it('reads claims when VTEX ID validates the JWT', async () => { + getAuthCookie.mockReturnValue('jwt-token') + parseJwt.mockReturnValue({ + isRepresentative: true, + customerId: 'cust-1', + unitId: 'unit-1', + }) + const validate = vi.fn().mockResolvedValue({ authStatus: 'Success' }) + const clients = { commerce: { vtexid: { validate } } } as any + + await expect( + resolveJwtClaims(clients, 'cookie', 'storeframework') + ).resolves.toEqual({ + isRepresentative: true, + customerId: 'cust-1', + unitId: 'unit-1', + }) + }) + + it('clears claims when VTEX ID validation throws', async () => { + getAuthCookie.mockReturnValue('jwt-token') + parseJwt.mockReturnValue({ + isRepresentative: true, + customerId: 'cust-1', + unitId: 'unit-1', + }) + const validate = vi.fn().mockRejectedValue(new Error('invalid')) + const clients = { commerce: { vtexid: { validate } } } as any + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + await expect( + resolveJwtClaims(clients, 'cookie', 'storeframework') + ).resolves.toEqual({ + isRepresentative: false, + customerId: undefined, + unitId: undefined, + }) + expect(warn).toHaveBeenCalled() + warn.mockRestore() + }) +}) diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 480a264c34..c61a9e840c 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -3,6 +3,48 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.6.0-dev.6](https://github.com/vtex/faststore/compare/v4.6.0-dev.5...v4.6.0-dev.6) (2026-08-14) + +### Bug Fixes + +- resolve node_modules bins when running scripts inside .faststore ([#3440](https://github.com/vtex/faststore/issues/3440)) ([2bf174a](https://github.com/vtex/faststore/commit/2bf174a7295d106354d97d7ca2c161ed706cff9a)), closes [#3439](https://github.com/vtex/faststore/issues/3439) [#3419](https://github.com/vtex/faststore/issues/3419) + +# [4.6.0-dev.5](https://github.com/vtex/faststore/compare/v4.6.0-dev.4...v4.6.0-dev.5) (2026-08-13) + +**Note:** Version bump only for package @faststore/cli + +# [4.6.0-dev.4](https://github.com/vtex/faststore/compare/v4.6.0-dev.3...v4.6.0-dev.4) (2026-08-12) + +### Bug Fixes + +- custom GraphQL typeDefs silently ignored on Windows ([#3419](https://github.com/vtex/faststore/issues/3419)) ([bdeb6a7](https://github.com/vtex/faststore/commit/bdeb6a757c44dda24b96cffc73d34652de165adf)) + +# [4.6.0-dev.3](https://github.com/vtex/faststore/compare/v4.6.0-dev.2...v4.6.0-dev.3) (2026-08-12) + +**Note:** Version bump only for package @faststore/cli + +# [4.6.0-dev.2](https://github.com/vtex/faststore/compare/v4.6.0-dev.1...v4.6.0-dev.2) (2026-08-12) + +**Note:** Version bump only for package @faststore/cli + +# [4.6.0-dev.1](https://github.com/vtex/faststore/compare/v4.6.0-dev.0...v4.6.0-dev.1) (2026-08-11) + +## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.5.1) (2026-08-11) + +**Note:** Version bump only for package @faststore/cli + +# [4.6.0-dev.0](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.6.0-dev.0) (2026-08-11) + +**Note:** Version bump only for package @faststore/cli + +## [4.5.1-dev.4](https://github.com/vtex/faststore/compare/v4.5.1-dev.3...v4.5.1-dev.4) (2026-08-10) + +### Bug Fixes + +- restore account, path, port arg order on cli dev command ([#3439](https://github.com/vtex/faststore/issues/3439)) ([b13e834](https://github.com/vtex/faststore/commit/b13e8341539fb8108c35a63f26218c4671d308f8)), closes [#3111](https://github.com/vtex/faststore/issues/3111) [#2491](https://github.com/vtex/faststore/issues/2491) [#2491](https://github.com/vtex/faststore/issues/2491) [#3111](https://github.com/vtex/faststore/issues/3111) + +## [4.5.1-dev.3](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1-dev.3) (2026-08-10) + ## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1) (2026-08-11) **Note:** Version bump only for package @faststore/cli diff --git a/packages/cli/README.md b/packages/cli/README.md index 5e94d09660..2e3111cc96 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -84,7 +84,7 @@ Versioning and publishing are managed at the monorepo root by Lerna. Do not publ * [`faststore cache-graphql [STORE]`](#faststore-cache-graphql-store) * [`faststore cms-sync [PATH]`](#faststore-cms-sync-path) * [`faststore create [PATH]`](#faststore-create-path) -* [`faststore dev [PATH] [ACCOUNT] [PORT]`](#faststore-dev-path-account-port) +* [`faststore dev [ACCOUNT] [PATH] [PORT]`](#faststore-dev-account-path-port) * [`faststore generate`](#faststore-generate) * [`faststore help [COMMAND]`](#faststore-help-command) * [`faststore prepare [PATH]`](#faststore-prepare-path) @@ -151,15 +151,15 @@ EXAMPLES $ yarn faststore create discovery ``` -## `faststore dev [PATH] [ACCOUNT] [PORT]` +## `faststore dev [ACCOUNT] [PATH] [PORT]` ``` USAGE - $ faststore dev [PATH] [ACCOUNT] [PORT] [--watch-plugins] + $ faststore dev [ACCOUNT] [PATH] [PORT] [--watch-plugins] ARGUMENTS - [PATH] The path where the FastStore being run is. Defaults to cwd. [ACCOUNT] The account for which the Discovery is running. Currently noop. + [PATH] The path where the FastStore being run is. Defaults to cwd. [PORT] The port where FastStore should run. Defaults to 3000. FLAGS diff --git a/packages/cli/package.json b/packages/cli/package.json index bbf2fd0ee3..9444708238 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/cli", - "version": "4.5.1", + "version": "4.6.0-dev.6", "description": "FastStore CLI", "author": "Emerson Laurentino @emersonlaurentino", "type": "module", diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 7094173500..606735e685 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -5,6 +5,7 @@ import { existsSync } from 'node:fs' import fsExtra from 'fs-extra' import path from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' +import { withNodeModulesBins } from '../utils/binPaths' import { getPreferredPackageManager } from '../utils/commands' import { checkDeprecatedSecretFiles } from '../utils/deprecations' import { getBasePath, withBasePath } from '../utils/directory' @@ -94,6 +95,7 @@ export default class Build extends Command { shell: true, cwd: tmpDir, stdio: 'inherit', + env: withNodeModulesBins(tmpDir), }) if (scriptResult.status && scriptResult.status !== 0) { diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index 142208ab06..ad0dc71a05 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -7,6 +7,7 @@ import dotenv from 'dotenv' import { cpSync, existsSync, readFileSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' +import { withNodeModulesBins } from '../utils/binPaths' import { getPreferredPackageManager } from '../utils/commands' import { checkDeprecatedSecretFiles } from '../utils/deprecations' import { getBasePath, withBasePath } from '../utils/directory' @@ -68,6 +69,7 @@ async function storeDev( errorMessage: `The "predev" step ("${packageManager} predev") failed inside the ".faststore" directory. See the error output below for details.`, throws: 'error', cwd: tmpDir, + env: withNodeModulesBins(tmpDir), }) const { success } = copyGenerated( @@ -89,10 +91,10 @@ async function storeDev( cwd: tmpDir, signal: devAbortController.signal, stdio: ['inherit', 'pipe', 'inherit'], - env: { + env: withNodeModulesBins(tmpDir, { ...process.env, ...envVars, - }, + }), }) let nextStdout = '' @@ -126,16 +128,16 @@ function copyGenerated(from: string, to: string) { export default class Dev extends Command { static args = { - path: Args.string({ - name: 'path', - description: - 'The path where the FastStore being run is. Defaults to cwd.', - }), account: Args.string({ name: 'account', description: 'The account for which the Discovery is running. Currently noop.', }), + path: Args.string({ + name: 'path', + description: + 'The path where the FastStore being run is. Defaults to cwd.', + }), port: Args.string({ name: 'port', description: 'The port where FastStore should run. Defaults to 3000.', diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index a45cd89e00..7e832eb6f3 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -2,6 +2,7 @@ import { Args, Command } from '@oclif/core' import { spawn } from 'node:child_process' import chokidar from 'chokidar' +import { withNodeModulesBins } from '../utils/binPaths' import { getPreferredPackageManager } from '../utils/commands' import { getBasePath, withBasePath } from '../utils/directory' import { generate } from '../utils/generate' @@ -37,6 +38,7 @@ async function storeTest(tmpDir: string) { cwd: tmpDir, signal: testAbortController.signal, stdio: 'inherit', + env: withNodeModulesBins(tmpDir), }) testProcess.on('close', () => { diff --git a/packages/cli/src/utils/binPaths.test.ts b/packages/cli/src/utils/binPaths.test.ts new file mode 100644 index 0000000000..dc75818c2b --- /dev/null +++ b/packages/cli/src/utils/binPaths.test.ts @@ -0,0 +1,148 @@ +import { spawnSync } from 'node:child_process' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +import { withNodeModulesBins } from './binPaths' + +// mimics a hoisted monorepo: bins live at the workspace root, the store has its +// own node_modules, and `.faststore` has none +let workspaceRoot: string +let storeDir: string +let tmpDir: string + +beforeAll(() => { + workspaceRoot = fs.realpathSync( + fs.mkdtempSync(path.join(os.tmpdir(), 'bin-paths-')) + ) + storeDir = path.join(workspaceRoot, 'stores', 'my-store') + tmpDir = path.join(storeDir, '.faststore') + + fs.mkdirSync(path.join(workspaceRoot, 'node_modules', '.bin'), { + recursive: true, + }) + fs.mkdirSync(path.join(storeDir, 'node_modules', '.bin'), { recursive: true }) + fs.mkdirSync(tmpDir, { recursive: true }) +}) + +afterAll(() => { + fs.rmSync(workspaceRoot, { recursive: true, force: true }) +}) + +describe('withNodeModulesBins', () => { + it('prepends the bin directories of every ancestor, nearest first', () => { + const { PATH } = withNodeModulesBins(tmpDir, { PATH: '/usr/bin' }) + const entries = PATH?.split(path.delimiter) ?? [] + + expect(entries.slice(0, 2)).toEqual([ + path.join(storeDir, 'node_modules', '.bin'), + path.join(workspaceRoot, 'node_modules', '.bin'), + ]) + expect(entries.at(-1)).toBe('/usr/bin') + }) + + it('skips ancestors without a node_modules/.bin', () => { + const { PATH } = withNodeModulesBins(tmpDir, { PATH: '/usr/bin' }) + + expect(PATH).not.toContain( + path.join(workspaceRoot, 'stores', 'node_modules', '.bin') + ) + }) + + it('keeps the other environment variables untouched', () => { + const env = withNodeModulesBins(tmpDir, { + PATH: '/usr/bin', + VTEX_ACCOUNT: 'storeframework', + }) + + expect(env.VTEX_ACCOUNT).toBe('storeframework') + }) + + it('reuses the existing PATH key casing, as used on Windows', () => { + const windowsPath = String.raw`C:\Windows\system32` + const env = withNodeModulesBins(tmpDir, { Path: windowsPath }) + + expect(env.PATH).toBeUndefined() + expect(env.Path).toContain(path.join(storeDir, 'node_modules', '.bin')) + expect(env.Path).toContain(windowsPath) + }) + + it('moves a bin directory already in PATH to its nearest-first position instead of duplicating it', () => { + const storeBinDir = path.join(storeDir, 'node_modules', '.bin') + const { PATH } = withNodeModulesBins(tmpDir, { + PATH: [storeBinDir, '/usr/bin'].join(path.delimiter), + }) + const entries = PATH?.split(path.delimiter) ?? [] + + expect(entries).toEqual([ + storeBinDir, + path.join(workspaceRoot, 'node_modules', '.bin'), + '/usr/bin', + ]) + }) + + it('does not drop the bin directories when PATH is empty', () => { + const { PATH } = withNodeModulesBins(tmpDir, {}) + + expect(PATH?.split(path.delimiter).slice(0, 2)).toEqual([ + path.join(storeDir, 'node_modules', '.bin'), + path.join(workspaceRoot, 'node_modules', '.bin'), + ]) + }) + + it('returns the environment untouched when no ancestor has a node_modules/.bin', () => { + // Sibling of the fixture rather than a descendant: the walk from here goes + // straight up the OS temp dir, where no node_modules/.bin exists. + const binlessDir = fs.realpathSync( + fs.mkdtempSync(path.join(os.tmpdir(), 'bin-paths-binless-')) + ) + const env = { PATH: '/usr/bin', VTEX_ACCOUNT: 'storeframework' } + + try { + expect(withNodeModulesBins(binlessDir, env)).toEqual(env) + } finally { + fs.rmSync(binlessDir, { recursive: true, force: true }) + } + }) + + // The tests above only inspect the object we build. This one spawns a real + // child process from `.faststore`, the way `dev` and `build` do, to check + // that the environment is what makes a bare binary name resolve. Windows + // would need a `.cmd` shim for the fixture, so it stays on POSIX. + it.skipIf(process.platform === 'win32')( + 'lets a child process resolve a binary living in an ancestor bin directory', + () => { + const probe = path.join( + storeDir, + 'node_modules', + '.bin', + 'faststore-bin-probe' + ) + // owner-only exec: the default mode is not executable, and the child + // process runs as the same user that writes the file + fs.writeFileSync(probe, '#!/bin/sh\necho resolved\n', { mode: 0o700 }) + + // no `shell: true`: the OS resolves the bare name from the environment + // we hand it, which is the behaviour under test, and it keeps a shell + // interpreter out of the test + const run = (env: NodeJS.ProcessEnv) => + spawnSync('faststore-bin-probe', { + cwd: tmpDir, + encoding: 'utf-8', + env, + }) + + // A PATH that cannot resolve the probe on its own, so that a passing + // assertion below can only come from the directories we added. + const barePath = { PATH: path.join(workspaceRoot, 'nowhere') } + + expect(run(barePath).status).not.toBe(0) + + const resolved = run(withNodeModulesBins(tmpDir, barePath)) + + expect(resolved.status).toBe(0) + expect(resolved.stdout.trim()).toBe('resolved') + } + ) +}) diff --git a/packages/cli/src/utils/binPaths.ts b/packages/cli/src/utils/binPaths.ts new file mode 100644 index 0000000000..858e150925 --- /dev/null +++ b/packages/cli/src/utils/binPaths.ts @@ -0,0 +1,65 @@ +import fsExtra from 'fs-extra' +import path from 'node:path' + +const { existsSync } = fsExtra + +function collectBinDirs(fromDir: string) { + const binDirs: string[] = [] + let currentDir = path.resolve(fromDir) + + while (true) { + const binDir = path.join(currentDir, 'node_modules', '.bin') + + if (existsSync(binDir)) { + binDirs.push(binDir) + } + + const parentDir = path.dirname(currentDir) + + if (parentDir === currentDir) { + return binDirs + } + + currentDir = parentDir + } +} + +// Environment variables are case-insensitive on Windows, where the key is +// usually `Path`. Reusing the existing key avoids handing the child process +// both `Path` and `PATH`. +function getPathKey(env: NodeJS.ProcessEnv) { + return Object.keys(env).find((key) => key.toUpperCase() === 'PATH') ?? 'PATH' +} + +/** + * Prepends the `node_modules/.bin` of `fromDir` and of every ancestor + * directory to `PATH`. + * + * The generated `.faststore` package has no `node_modules` of its own, so the + * binaries its scripts call (`na`, `next`) live higher up the tree: in the + * store root or, on hoisted monorepos, the workspace root. + */ +export function withNodeModulesBins( + fromDir: string, + env: NodeJS.ProcessEnv = process.env +): NodeJS.ProcessEnv { + const binDirs = collectBinDirs(fromDir) + + if (binDirs.length === 0) { + return { ...env } + } + + const pathKey = getPathKey(env) + const currentEntries = env[pathKey]?.split(path.delimiter) ?? [] + // Package managers already put some of these directories in PATH when they + // run a script, but not necessarily nearest first. Dropping their copies + // before prepending `binDirs` keeps the nearest ancestor in charge instead + // of letting a farther one that was missing from PATH jump ahead of it. + const binDirSet = new Set(binDirs) + const otherEntries = currentEntries.filter((entry) => !binDirSet.has(entry)) + + return { + ...env, + [pathKey]: [...binDirs, ...otherEntries].join(path.delimiter), + } +} diff --git a/packages/cli/src/utils/generate-types.test.ts b/packages/cli/src/utils/generate-types.test.ts new file mode 100644 index 0000000000..10fbca12d6 --- /dev/null +++ b/packages/cli/src/utils/generate-types.test.ts @@ -0,0 +1,67 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { Kind } from 'graphql' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +import { getTypeDefsFromFolder } from './generate-types' + +// creates /src/graphql/thirdParty/typeDefs/hello.graphql in a fresh +// temp dir whose name starts with the given prefix +function makeFixture(prefix: string) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix)) + + const typeDefsDir = path.join( + root, + 'src', + 'graphql', + 'thirdParty', + 'typeDefs' + ) + fs.mkdirSync(typeDefsDir, { recursive: true }) + fs.writeFileSync( + path.join(typeDefsDir, 'hello.graphql'), + 'extend type Query { hello: String! }' + ) + + return root +} + +describe('getTypeDefsFromFolder', () => { + // native separators on purpose — on Windows this produces backslashes, + // which reproduces the bug where the globby pattern matched nothing + let root: string + // parentheses are glob-special characters, so a raw path.join pattern + // matches nothing on POSIX too — this guards the regression on Linux CI + let globSpecialRoot: string + + beforeAll(() => { + root = makeFixture('faststore-typedefs-') + globSpecialRoot = makeFixture('faststore-typedefs-(x86)-') + }) + + afterAll(() => { + fs.rmSync(root, { recursive: true, force: true }) + fs.rmSync(globSpecialRoot, { recursive: true, force: true }) + }) + + it('finds and parses custom typeDefs when root uses native path separators', async () => { + const typeDefs = await getTypeDefsFromFolder(root, 'thirdParty') + + expect(typeDefs).toHaveLength(1) + expect(typeDefs[0].kind).toBe(Kind.DOCUMENT) + }) + + it('finds custom typeDefs when the root path contains glob-special characters', async () => { + const typeDefs = await getTypeDefsFromFolder(globSpecialRoot, 'thirdParty') + + expect(typeDefs).toHaveLength(1) + expect(typeDefs[0].kind).toBe(Kind.DOCUMENT) + }) + + it('returns an empty array when the custom folder has no typeDefs', async () => { + const typeDefs = await getTypeDefsFromFolder(root, 'vtex') + + expect(typeDefs).toHaveLength(0) + }) +}) diff --git a/packages/cli/src/utils/generate-types.ts b/packages/cli/src/utils/generate-types.ts index fe1ca7ed36..db877f7715 100644 --- a/packages/cli/src/utils/generate-types.ts +++ b/packages/cli/src/utils/generate-types.ts @@ -132,18 +132,31 @@ async function generateSchemaFile(rootPath: string) { saveSchemaFile(finalSchema) } -async function getTypeDefsFromFolder(root: string, customPath: string) { - const globby = await import('globby') +type GlobbyModule = typeof import('globby') + +export async function getTypeDefsFromFolder( + root: string, + customPath: string | string[] +) { + const globbyImport = (await import('globby')) as GlobbyModule & { + default?: GlobbyModule + } + const globbyModule = globbyImport.default ?? globbyImport const basePath = [root, 'src', 'graphql'] const pathArray = Array.isArray(customPath) ? customPath : [customPath] - return ((globby as any).default ?? globby) - .globbySync(path.join(...[...basePath, ...pathArray]), { - expandDirectories: { - extensions: ['graphql'], - }, - }) + // globby patterns must use forward slashes — on Windows, path.join produces + // backslashes, which globby treats as escape characters and matches nothing + const pattern = globbyModule.convertPathToPattern( + path.join(...basePath, ...pathArray) + ) + + // spell out the glob instead of using expandDirectories: it stats the + // escaped pattern string, so it never expands paths that needed escaping + // (e.g. parentheses in a parent directory name) + return globbyModule + .globbySync(`${pattern}/**/*.graphql`) .map((typeDef: string) => parse(fs.readFileSync(typeDef, { encoding: 'utf-8' })) ) diff --git a/packages/cli/src/utils/runCommandSync.test.ts b/packages/cli/src/utils/runCommandSync.test.ts new file mode 100644 index 0000000000..2147f4260b --- /dev/null +++ b/packages/cli/src/utils/runCommandSync.test.ts @@ -0,0 +1,47 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { runCommandSync } from './runCommandSync' + +const execSyncMock = vi.hoisted(() => vi.fn()) + +vi.mock('node:child_process', () => ({ execSync: execSyncMock })) + +const optionsOfLastCall = () => execSyncMock.mock.calls.at(-1)?.[1] + +describe('runCommandSync', () => { + beforeEach(() => { + execSyncMock.mockClear() + }) + + it('forwards a custom environment to the child process', () => { + const env = { + PATH: '/store/node_modules/.bin', + VTEX_ACCOUNT: 'storeframework', + } + + runCommandSync({ + cmd: 'yarn predev', + errorMessage: 'predev failed', + throws: 'error', + cwd: '/store/.faststore', + env, + }) + + expect(optionsOfLastCall()).toMatchObject({ + cwd: '/store/.faststore', + env, + }) + }) + + // execSync treats an undefined env as "inherit process.env", which is what + // the call sites that never pass one rely on. + it('leaves the environment to the child when the caller does not pass one', () => { + runCommandSync({ + cmd: 'yarn generate', + errorMessage: 'generate failed', + throws: 'error', + }) + + expect(optionsOfLastCall()?.env).toBeUndefined() + }) +}) diff --git a/packages/cli/src/utils/runCommandSync.ts b/packages/cli/src/utils/runCommandSync.ts index 2b2bbac07b..b9d2cd8068 100644 --- a/packages/cli/src/utils/runCommandSync.ts +++ b/packages/cli/src/utils/runCommandSync.ts @@ -46,12 +46,14 @@ export const runCommandSync = ({ errorMessage, throws, cwd, + env, interactive = false, }: { cmd: string errorMessage: string throws: 'warning' | 'error' cwd?: string + env?: NodeJS.ProcessEnv interactive?: boolean }) => { try { @@ -70,6 +72,7 @@ export const runCommandSync = ({ const res = execSync(interactive ? cmd : `${cmd} 2>&1`, { stdio: interactive ? 'inherit' : 'pipe', cwd, + env, }) logger.log(`[STATUS] ${res?.toString() ?? 'Unknown'}`) logger.log(`[FINISHED] ${cmd}`) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index c12a121434..f9d3890eb7 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -3,6 +3,42 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.6.0-dev.6](https://github.com/vtex/faststore/compare/v4.6.0-dev.5...v4.6.0-dev.6) (2026-08-14) + +**Note:** Version bump only for package @faststore/components + +# [4.6.0-dev.5](https://github.com/vtex/faststore/compare/v4.6.0-dev.4...v4.6.0-dev.5) (2026-08-13) + +**Note:** Version bump only for package @faststore/components + +# [4.6.0-dev.4](https://github.com/vtex/faststore/compare/v4.6.0-dev.3...v4.6.0-dev.4) (2026-08-12) + +**Note:** Version bump only for package @faststore/components + +# [4.6.0-dev.3](https://github.com/vtex/faststore/compare/v4.6.0-dev.2...v4.6.0-dev.3) (2026-08-12) + +**Note:** Version bump only for package @faststore/components + +# [4.6.0-dev.2](https://github.com/vtex/faststore/compare/v4.6.0-dev.1...v4.6.0-dev.2) (2026-08-12) + +**Note:** Version bump only for package @faststore/components + +# [4.6.0-dev.1](https://github.com/vtex/faststore/compare/v4.6.0-dev.0...v4.6.0-dev.1) (2026-08-11) + +## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.5.1) (2026-08-11) + +**Note:** Version bump only for package @faststore/components + +# [4.6.0-dev.0](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.6.0-dev.0) (2026-08-11) + +**Note:** Version bump only for package @faststore/components + +## [4.5.1-dev.4](https://github.com/vtex/faststore/compare/v4.5.1-dev.3...v4.5.1-dev.4) (2026-08-10) + +**Note:** Version bump only for package @faststore/components + +## [4.5.1-dev.3](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1-dev.3) (2026-08-10) + ## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1) (2026-08-11) **Note:** Version bump only for package @faststore/components diff --git a/packages/components/package.json b/packages/components/package.json index 1a868b56c2..3c49b885d8 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/components", - "version": "4.5.1", + "version": "4.6.0-dev.6", "main": "dist/cjs/index.js", "module": "dist/es/index.mjs", "typings": "dist/index.d.ts", diff --git a/packages/core/@generated/gql.ts b/packages/core/@generated/gql.ts index c18bc3146c..9b13e3ea62 100644 --- a/packages/core/@generated/gql.ts +++ b/packages/core/@generated/gql.ts @@ -45,7 +45,7 @@ type Documents = { "\n mutation ProcessOrderAuthorizationMutation($data: IProcessOrderAuthorization!) {\n processOrderAuthorization(data: $data) {\n isPendingForOtherAuthorizer\n ruleForAuthorization {\n orderAuthorizationId\n dimensionId\n rule {\n id\n name\n status\n doId\n authorizedEmails\n priority\n trigger {\n condition {\n conditionType\n description\n lessThan\n greatherThan\n expression\n }\n effect {\n description\n effectType\n funcPath\n }\n }\n timeout\n notification\n scoreInterval {\n accept\n deny\n }\n authorizationData {\n requireAllApprovals\n authorizers {\n id\n email\n type\n authorizationDate\n }\n }\n isUserAuthorized\n isUserNextAuthorizer\n }\n }\n }\n }\n": typeof types.ProcessOrderAuthorizationMutationDocument, "\n query ValidateUser {\n validateUser {\n isValid\n }\n }\n": typeof types.ValidateUserDocument, "\n mutation StartRecommendationSession {\n startRecommendationSession\n }\n": typeof types.StartRecommendationSessionDocument, - "\n mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) {\n validateCart(cart: $cart, session: $session) {\n order {\n orderNumber\n acceptedOffer {\n ...CartItem\n }\n shouldSplitItem\n }\n messages {\n ...CartMessage\n }\n }\n }\n\n fragment CartMessage on StoreCartMessage {\n text\n status\n }\n\n fragment CartItem on StoreOffer {\n seller {\n identifier\n }\n quantity\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n isGift\n priceToken\n itemOffered {\n ...CartProductItem\n }\n }\n\n fragment CartProductItem on StoreProduct {\n sku\n name\n unitMultiplier\n image {\n url\n alternateName\n }\n brand {\n name\n }\n isVariantOf {\n productGroupID\n name\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n gtin\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n": typeof types.ValidateCartMutationDocument, + "\n mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) {\n validateCart(cart: $cart, session: $session) {\n order {\n orderNumber\n salesChannel\n acceptedOffer {\n ...CartItem\n }\n shouldSplitItem\n }\n messages {\n ...CartMessage\n }\n }\n }\n\n fragment CartMessage on StoreCartMessage {\n text\n status\n }\n\n fragment CartItem on StoreOffer {\n seller {\n identifier\n }\n quantity\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n isGift\n priceToken\n itemOffered {\n ...CartProductItem\n }\n }\n\n fragment CartProductItem on StoreProduct {\n sku\n name\n unitMultiplier\n image {\n url\n alternateName\n }\n brand {\n name\n }\n isVariantOf {\n productGroupID\n name\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n gtin\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n": typeof types.ValidateCartMutationDocument, "\n query ClientPickupPointsQuery(\n $geoCoordinates: IStoreGeoCoordinates\n ) {\n pickupPoints(geoCoordinates: $geoCoordinates) {\n pickupPointDistances {\n pickupId\n distance\n pickupName\n isActive\n address {\n city\n state\n number\n postalCode\n street\n }\n }\n }\n }\n": typeof types.ClientPickupPointsQueryDocument, "\n mutation SubscribeToNewsletter($data: IPersonNewsletter!) {\n subscribeToNewsletter(data: $data) {\n id\n }\n }\n": typeof types.SubscribeToNewsletterDocument, "\n mutation StartOrderEntryOperationMutation($data: IOrderEntryOperation!) {\n startOrderEntryOperation(data: $data) {\n operationId\n }\n }\n": typeof types.StartOrderEntryOperationMutationDocument, @@ -97,7 +97,7 @@ const documents: Documents = { "\n mutation ProcessOrderAuthorizationMutation($data: IProcessOrderAuthorization!) {\n processOrderAuthorization(data: $data) {\n isPendingForOtherAuthorizer\n ruleForAuthorization {\n orderAuthorizationId\n dimensionId\n rule {\n id\n name\n status\n doId\n authorizedEmails\n priority\n trigger {\n condition {\n conditionType\n description\n lessThan\n greatherThan\n expression\n }\n effect {\n description\n effectType\n funcPath\n }\n }\n timeout\n notification\n scoreInterval {\n accept\n deny\n }\n authorizationData {\n requireAllApprovals\n authorizers {\n id\n email\n type\n authorizationDate\n }\n }\n isUserAuthorized\n isUserNextAuthorizer\n }\n }\n }\n }\n": types.ProcessOrderAuthorizationMutationDocument, "\n query ValidateUser {\n validateUser {\n isValid\n }\n }\n": types.ValidateUserDocument, "\n mutation StartRecommendationSession {\n startRecommendationSession\n }\n": types.StartRecommendationSessionDocument, - "\n mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) {\n validateCart(cart: $cart, session: $session) {\n order {\n orderNumber\n acceptedOffer {\n ...CartItem\n }\n shouldSplitItem\n }\n messages {\n ...CartMessage\n }\n }\n }\n\n fragment CartMessage on StoreCartMessage {\n text\n status\n }\n\n fragment CartItem on StoreOffer {\n seller {\n identifier\n }\n quantity\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n isGift\n priceToken\n itemOffered {\n ...CartProductItem\n }\n }\n\n fragment CartProductItem on StoreProduct {\n sku\n name\n unitMultiplier\n image {\n url\n alternateName\n }\n brand {\n name\n }\n isVariantOf {\n productGroupID\n name\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n gtin\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n": types.ValidateCartMutationDocument, + "\n mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) {\n validateCart(cart: $cart, session: $session) {\n order {\n orderNumber\n salesChannel\n acceptedOffer {\n ...CartItem\n }\n shouldSplitItem\n }\n messages {\n ...CartMessage\n }\n }\n }\n\n fragment CartMessage on StoreCartMessage {\n text\n status\n }\n\n fragment CartItem on StoreOffer {\n seller {\n identifier\n }\n quantity\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n isGift\n priceToken\n itemOffered {\n ...CartProductItem\n }\n }\n\n fragment CartProductItem on StoreProduct {\n sku\n name\n unitMultiplier\n image {\n url\n alternateName\n }\n brand {\n name\n }\n isVariantOf {\n productGroupID\n name\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n gtin\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n": types.ValidateCartMutationDocument, "\n query ClientPickupPointsQuery(\n $geoCoordinates: IStoreGeoCoordinates\n ) {\n pickupPoints(geoCoordinates: $geoCoordinates) {\n pickupPointDistances {\n pickupId\n distance\n pickupName\n isActive\n address {\n city\n state\n number\n postalCode\n street\n }\n }\n }\n }\n": types.ClientPickupPointsQueryDocument, "\n mutation SubscribeToNewsletter($data: IPersonNewsletter!) {\n subscribeToNewsletter(data: $data) {\n id\n }\n }\n": types.SubscribeToNewsletterDocument, "\n mutation StartOrderEntryOperationMutation($data: IOrderEntryOperation!) {\n startOrderEntryOperation(data: $data) {\n operationId\n }\n }\n": types.StartOrderEntryOperationMutationDocument, @@ -242,7 +242,7 @@ export function gql(source: "\n mutation StartRecommendationSession {\n star /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function gql(source: "\n mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) {\n validateCart(cart: $cart, session: $session) {\n order {\n orderNumber\n acceptedOffer {\n ...CartItem\n }\n shouldSplitItem\n }\n messages {\n ...CartMessage\n }\n }\n }\n\n fragment CartMessage on StoreCartMessage {\n text\n status\n }\n\n fragment CartItem on StoreOffer {\n seller {\n identifier\n }\n quantity\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n isGift\n priceToken\n itemOffered {\n ...CartProductItem\n }\n }\n\n fragment CartProductItem on StoreProduct {\n sku\n name\n unitMultiplier\n image {\n url\n alternateName\n }\n brand {\n name\n }\n isVariantOf {\n productGroupID\n name\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n gtin\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n"): typeof import('./graphql').ValidateCartMutationDocument; +export function gql(source: "\n mutation ValidateCartMutation($cart: IStoreCart!, $session: IStoreSession!) {\n validateCart(cart: $cart, session: $session) {\n order {\n orderNumber\n salesChannel\n acceptedOffer {\n ...CartItem\n }\n shouldSplitItem\n }\n messages {\n ...CartMessage\n }\n }\n }\n\n fragment CartMessage on StoreCartMessage {\n text\n status\n }\n\n fragment CartItem on StoreOffer {\n seller {\n identifier\n }\n quantity\n price\n priceWithTaxes\n listPrice\n listPriceWithTaxes\n isGift\n priceToken\n itemOffered {\n ...CartProductItem\n }\n }\n\n fragment CartProductItem on StoreProduct {\n sku\n name\n unitMultiplier\n image {\n url\n alternateName\n }\n brand {\n name\n }\n isVariantOf {\n productGroupID\n name\n skuVariants {\n activeVariations\n slugsMap\n availableVariations\n }\n }\n gtin\n additionalProperty {\n propertyID\n name\n value\n valueReference\n }\n }\n"): typeof import('./graphql').ValidateCartMutationDocument; /** * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/core/@generated/graphql.ts b/packages/core/@generated/graphql.ts index 27d861e8fd..f39891dc5a 100644 --- a/packages/core/@generated/graphql.ts +++ b/packages/core/@generated/graphql.ts @@ -1538,6 +1538,12 @@ export type StoreOrder = { acceptedOffer: Array; /** ID of the order in [VTEX order management](https://help.vtex.com/en/tutorial/license-manager-resources-oms--60QcBsvWeum02cFi3GjBzg#). */ orderNumber: Scalars['String']['output']; + /** + * Sales channel of the underlying orderForm when FastStore adopted it because + * the browser session was stale (e.g. after Quick Order). Clients should align + * `session.channel` to this value. Null when no SC adoption happened. + */ + salesChannel: Maybe; /** Indicates whether or not items with attachments should be split. */ shouldSplitItem: Maybe; }; @@ -2863,7 +2869,7 @@ export type ValidateCartMutationMutationVariables = Exact<{ }>; -export type ValidateCartMutationMutation = { validateCart: { order: { orderNumber: string, shouldSplitItem: boolean | null, acceptedOffer: Array<{ quantity: number, price: number, priceWithTaxes: number, listPrice: number, listPriceWithTaxes: number, isGift: boolean | null, priceToken: string | null, seller: { identifier: string }, itemOffered: { sku: string, name: string, unitMultiplier: number | null, gtin: string, image: Array<{ url: string, alternateName: string }>, brand: { name: string }, isVariantOf: { productGroupID: string, name: string, skuVariants: { activeVariations: any | null, slugsMap: any | null, availableVariations: any | null } | null }, additionalProperty: Array<{ propertyID: string, name: string, value: any, valueReference: any }> } }> }, messages: Array<{ text: string, status: StoreStatus }> } | null }; +export type ValidateCartMutationMutation = { validateCart: { order: { orderNumber: string, salesChannel: string | null, shouldSplitItem: boolean | null, acceptedOffer: Array<{ quantity: number, price: number, priceWithTaxes: number, listPrice: number, listPriceWithTaxes: number, isGift: boolean | null, priceToken: string | null, seller: { identifier: string }, itemOffered: { sku: string, name: string, unitMultiplier: number | null, gtin: string, image: Array<{ url: string, alternateName: string }>, brand: { name: string }, isVariantOf: { productGroupID: string, name: string, skuVariants: { activeVariations: any | null, slugsMap: any | null, availableVariations: any | null } | null }, additionalProperty: Array<{ propertyID: string, name: string, value: any, valueReference: any }> } }> }, messages: Array<{ text: string, status: StoreStatus }> } | null }; export type CartMessageFragment = { text: string, status: StoreStatus }; @@ -3565,7 +3571,7 @@ export const CancelOrderMutationDocument = {"__meta__":{"operationName":"CancelO export const ProcessOrderAuthorizationMutationDocument = {"__meta__":{"operationName":"ProcessOrderAuthorizationMutation","operationHash":"8c25d37c8d6e7c20ab21bb8a4f4e6a2fe320ea8d"}} as unknown as TypedDocumentString; export const ValidateUserDocument = {"__meta__":{"operationName":"ValidateUser","operationHash":"32f99c73c3de958b64d6bece1afe800469f54548"}} as unknown as TypedDocumentString; export const StartRecommendationSessionDocument = {"__meta__":{"operationName":"StartRecommendationSession","operationHash":"1def6438c0cd87b85002411ac7326c221f192583"}} as unknown as TypedDocumentString; -export const ValidateCartMutationDocument = {"__meta__":{"operationName":"ValidateCartMutation","operationHash":"8199d03debb0a6751ec53d8ee8987b78d0292acb"}} as unknown as TypedDocumentString; +export const ValidateCartMutationDocument = {"__meta__":{"operationName":"ValidateCartMutation","operationHash":"3c71b0d3fc2bade14df68abfa271858beb361b12"}} as unknown as TypedDocumentString; export const ClientPickupPointsQueryDocument = {"__meta__":{"operationName":"ClientPickupPointsQuery","operationHash":"3fa04e88c811fcb5ece7206fd5aa745bdbc143a8"}} as unknown as TypedDocumentString; export const SubscribeToNewsletterDocument = {"__meta__":{"operationName":"SubscribeToNewsletter","operationHash":"feb7005103a859e2bc8cf2360d568806fd88deba"}} as unknown as TypedDocumentString; export const StartOrderEntryOperationMutationDocument = {"__meta__":{"operationName":"StartOrderEntryOperationMutation","operationHash":"78c50fbf9b85d03dbeac9b05b06405217f2ec440"}} as unknown as TypedDocumentString; diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index d7354e76b8..80727a2e13 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -3,10 +3,76 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1) (2026-08-11) +# [4.6.0-dev.6](https://github.com/vtex/faststore/compare/v4.6.0-dev.5...v4.6.0-dev.6) (2026-08-14) + +**Note:** Version bump only for package @faststore/core + +# [4.6.0-dev.5](https://github.com/vtex/faststore/compare/v4.6.0-dev.4...v4.6.0-dev.5) (2026-08-13) + +### Bug Fixes + +- **core:** add experimental.revalidate404 and skip PLP query on notFound ([855a3ee](https://github.com/vtex/faststore/commit/855a3ee2e3393ee092d4b261ad83a8757289e132)) +- **core:** revalidate ISR cache for notFound PDP/PLP paths ([1d1015e](https://github.com/vtex/faststore/commit/1d1015e8874d794c5f54e572afc3c676fabce69c)) + +# [4.6.0-dev.4](https://github.com/vtex/faststore/compare/v4.6.0-dev.3...v4.6.0-dev.4) (2026-08-12) + +**Note:** Version bump only for package @faststore/core + +# [4.6.0-dev.3](https://github.com/vtex/faststore/compare/v4.6.0-dev.2...v4.6.0-dev.3) (2026-08-12) + +**Note:** Version bump only for package @faststore/core + +# [4.6.0-dev.2](https://github.com/vtex/faststore/compare/v4.6.0-dev.1...v4.6.0-dev.2) (2026-08-12) + +### Bug Fixes + +- **api:** address Sonar and harden session sales-channel sync ([5f6e8e9](https://github.com/vtex/faststore/commit/5f6e8e909def75f60c24cdaffae9303ed2c0a052)) +- **api:** keep orderForm sales channel after stale validateCart sync ([09b051b](https://github.com/vtex/faststore/commit/09b051b80aaff7183adf7f5e86aece438bcd38f0)) +- **api:** preserve orderForm sales channel in validateCart ([2336cda](https://github.com/vtex/faststore/commit/2336cda2c95970850dd0d29c1c18043ca8c9078a)) +- **api:** sync adopted orderForm sales channel into session ([8943da7](https://github.com/vtex/faststore/commit/8943da7821f8abec08a39b7907a258bf156b05ee)) + +# [4.6.0-dev.1](https://github.com/vtex/faststore/compare/v4.6.0-dev.0...v4.6.0-dev.1) (2026-08-11) + +## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.5.1) (2026-08-11) + +### Bug Fixes + +- **core:** address scroll restoration review feedback ([a5fe6a3](https://github.com/vtex/faststore/commit/a5fe6a35c54e6095f81c7995a815ce6ef3b4dd4e)) +- **core:** align beforePopState callback with NextHistoryState ([10719e5](https://github.com/vtex/faststore/commit/10719e5eca199385d9fe18bdd6d7da290716a1d5)) +- **core:** compare window to undefined directly for Sonar ([bf05a54](https://github.com/vtex/faststore/commit/bf05a54bb014d120b7bdc95ff7c01a4e9b35b485)) +- **core:** harden scroll restore path matching and hydration-safe heights ([34efa6b](https://github.com/vtex/faststore/commit/34efa6bda7549895a4531277c2fe55bd6bbe98a7)) +- **core:** hash gallery height keys to avoid truncation collisions ([9e98043](https://github.com/vtex/faststore/commit/9e980438d74e457fa33aa77900438339463a078c)) +- **core:** prevent IS redirect hang and improve scroll restoration ([5f30778](https://github.com/vtex/faststore/commit/5f30778161eec2e4381b3fe971479dd7cbe53b08)) +- **core:** reset infinite scroll on forward listing navigations ([249226a](https://github.com/vtex/faststore/commit/249226a3acbe4cb26d687cf962125b2593d5949e)) +- **core:** resolve Sonar new-code smells for scroll restoration ([59b41ed](https://github.com/vtex/faststore/commit/59b41ed1d2a30ba693726b19895a0279eb1ba464)) +- **core:** restore PLP infinite-scroll pages on back navigation ([92b2efa](https://github.com/vtex/faststore/commit/92b2efa1d4d0895a10b5a0e71a4d9bf37cae1686)) +- **core:** resync gallery height lock when children change ([5058089](https://github.com/vtex/faststore/commit/50580898d97baf800b97c8a876f7967529859c61)) +- **core:** satisfy Sonar on gallery height key hash ([3a6ce47](https://github.com/vtex/faststore/commit/3a6ce47485c26be75d6d76906df3af64c9e34fd6)) +- **core:** type scroll restore paint timer for DOM/Node setTimeout ([940c903](https://github.com/vtex/faststore/commit/940c90376f8d2eb77665e73a6fb0ea53c9786c8e)) +- **core:** use replaceAll for infinite-scroll session key ([4f52048](https://github.com/vtex/faststore/commit/4f520482a9ba9661782fcf25a286b8f2e6c6b922)) + +# [4.6.0-dev.0](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.6.0-dev.0) (2026-08-11) + +### Features + +- removing vtex/diagnostics-nodejs ([#3389](https://github.com/vtex/faststore/issues/3389)) ([74e2d50](https://github.com/vtex/faststore/commit/74e2d503a44903929b90af7701956536690d4e41)) + +## [4.5.1-dev.4](https://github.com/vtex/faststore/compare/v4.5.1-dev.3...v4.5.1-dev.4) (2026-08-10) + +**Note:** Version bump only for package @faststore/core + +## [4.5.1-dev.3](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1-dev.3) (2026-08-10) ### Bug Fixes +- **core:** add twitter card meta tag to default next seo config ([720f3e6](https://github.com/vtex/faststore/commit/720f3e6d04ef523f2fcf0e146c3741b7e5af70fa)) +- **core:** add twittermeta tags validaton for tests ([c8f5872](https://github.com/vtex/faststore/commit/c8f5872e42b69ddae5b2abfa0e8d3140e2bc7163)) +- **core:** lint error ([d07460f](https://github.com/vtex/faststore/commit/d07460f890ba712c9aecbec25bda5de96bf3e4ef)) +- **core:** remove duplicated twitter meta tag ([f388288](https://github.com/vtex/faststore/commit/f388288731ff75fceedba3cd7449ef1088b0b0c2)) +- **metaTags:** add twitter meta tags to nextSeo ([9c2439b](https://github.com/vtex/faststore/commit/9c2439b4a668dd81b7bfe5bdc44df575725067ec)) + +## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1) (2026-08-11) + - **core:** address scroll restoration review feedback ([a5fe6a3](https://github.com/vtex/faststore/commit/a5fe6a35c54e6095f81c7995a815ce6ef3b4dd4e)) - **core:** align beforePopState callback with NextHistoryState ([10719e5](https://github.com/vtex/faststore/commit/10719e5eca199385d9fe18bdd6d7da290716a1d5)) - **core:** compare window to undefined directly for Sonar ([bf05a54](https://github.com/vtex/faststore/commit/bf05a54bb014d120b7bdc95ff7c01a4e9b35b485)) diff --git a/packages/core/README.md b/packages/core/README.md index 471554895f..ead0f55ee1 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -31,7 +31,7 @@ packages/core/ │ ├── server/ # Server-side utilities (CMS content fetching) │ ├── customizations/ # Store-level overrides: styles, fonts, components, fragments │ ├── styles/ # Global SCSS styles -│ └── instrumentation.ts # Next.js instrumentation hook (boots @faststore/diagnostics when otelEnabled) +│ └── instrumentation.ts # Next.js instrumentation hook (boots @faststore/diagnostics — see docs/observability.md) ├── cms/faststore/ # CMS configuration: sections.json, content-types.json, schemas ├── @generated/ # Auto-generated GraphQL types — do not edit ├── discovery.config.default.js # Committed base store configuration (platform, sales channel, locale) diff --git a/packages/core/cypress/integration/seo.test.js b/packages/core/cypress/integration/seo.test.js index 00a619d74e..23fcce59a4 100644 --- a/packages/core/cypress/integration/seo.test.js +++ b/packages/core/cypress/integration/seo.test.js @@ -47,7 +47,7 @@ describe('Home Page Seo', () => { }) }) - it('has OpenGraph tags', () => { + it('has OpenGraph and Twitter tags', () => { cy.visit(pages.home, options) cy.waitForHydration() @@ -68,6 +68,12 @@ describe('Home Page Seo', () => { .should(($el) => { expect($el.attr('content')).to.be.a('string') }) + + cy.get('meta[name="twitter:card"]') + .should('exist') + .should(($el) => { + expect($el.attr('content')).to.eq('summary_large_image') + }) }) }) @@ -117,7 +123,7 @@ describe('Product Page Seo', () => { }) }) - it('has OpenGraph tags', () => { + it('has OpenGraph and Twitter tags', () => { cy.visit(pages.pdp, options) cy.waitForHydration() @@ -156,6 +162,12 @@ describe('Product Page Seo', () => { .should(($el) => { expect($el.attr('content')).to.be.a('string') }) + + cy.get('meta[name="twitter:card"]') + .should('exist') + .should(($el) => { + expect($el.attr('content')).to.eq('summary_large_image') + }) }) }) @@ -201,7 +213,7 @@ describe('Collection Page Seo', () => { }) }) - it('has OpenGraph tags', () => { + it('has OpenGraph and Twitter tags', () => { cy.visit(pages.collection, options) cy.waitForHydration() @@ -222,6 +234,12 @@ describe('Collection Page Seo', () => { .should(($el) => { expect($el.attr('content')).to.be.a('string') }) + + cy.get('meta[name="twitter:card"]') + .should('exist') + .should(($el) => { + expect($el.attr('content')).to.eq('summary_large_image') + }) }) }) @@ -256,7 +274,7 @@ describe('Filtered Collection Page Seo', () => { }) }) - it('has OpenGraph tags', () => { + it('has OpenGraph and Twitter tags', () => { cy.visit(pages.collection_filtered, options) cy.waitForHydration() @@ -277,6 +295,12 @@ describe('Filtered Collection Page Seo', () => { .should(($el) => { expect($el.attr('content')).to.be.a('string') }) + + cy.get('meta[name="twitter:card"]') + .should('exist') + .should(($el) => { + expect($el.attr('content')).to.eq('summary_large_image') + }) }) }) @@ -295,7 +319,7 @@ describe('Search Page Seo', () => { cy.get('link[rel="canonical"]').should('not.exist') }) - it('has OpenGraph tags', () => { + it('has OpenGraph and Twitter tags', () => { cy.visit(pages.search, options) cy.waitForHydration() @@ -316,5 +340,11 @@ describe('Search Page Seo', () => { .should(($el) => { expect($el.attr('content')).to.be.a('string') }) + + cy.get('meta[name="twitter:card"]') + .should('exist') + .should(($el) => { + expect($el.attr('content')).to.eq('summary_large_image') + }) }) }) diff --git a/packages/core/discovery.config.default.js b/packages/core/discovery.config.default.js index db951b6610..bab63f6fbf 100644 --- a/packages/core/discovery.config.default.js +++ b/packages/core/discovery.config.default.js @@ -166,8 +166,7 @@ module.exports = { analytics: { // https://developers.google.com/tag-platform/tag-manager/web#standard_web_page_installation, gtmContainerId: 'GTM-PGHZ95N', - serviceName: 'faststore-proxy', - otelEnabled: true, + serviceName: 'faststore', }, cms: { @@ -192,6 +191,8 @@ module.exports = { nofollow: false, preact: false, enableRedirects: false, + // ISR window (seconds) for notFound PDP/PLP paths. + revalidate404: 60 * 5, // 5 minutes enableSearchSSR: false, enableFaststoreMyAccount: false, useIsGiftFromOrderForm: false, diff --git a/packages/core/next-seo.config.ts b/packages/core/next-seo.config.ts index d95d355959..72e6ed4073 100644 --- a/packages/core/next-seo.config.ts +++ b/packages/core/next-seo.config.ts @@ -8,6 +8,9 @@ const config: DefaultSeoProps = { norobots: storeConfig.experimental.noRobots, dangerouslySetAllPagesToNoFollow: storeConfig.experimental.nofollow, dangerouslySetAllPagesToNoIndex: storeConfig.experimental.noindex, + twitter: { + cardType: 'summary_large_image', + }, additionalMetaTags: [ { name: 'generated-at', diff --git a/packages/core/next.config.js b/packages/core/next.config.js index c384be0da0..3ac4bd628a 100644 --- a/packages/core/next.config.js +++ b/packages/core/next.config.js @@ -14,7 +14,6 @@ const getRootFolder = () => { console.log(` Root folder at: ${getRootFolder()} AccountID: ${storeConfig.api?.storeId ?? 'Unknown'} - Analytics Enabled: ${storeConfig.analytics?.otelEnabled ?? false} `) /** @@ -22,8 +21,6 @@ console.log(` * */ const nextConfig = { /* config options here */ - /* Replaces terser by swc for minifying. It's the default in NextJS 13 */ - swcMinify: true, ...(Array.isArray(storeConfig.experimental?.transpilePackages) && storeConfig.experimental.transpilePackages.length > 0 && { transpilePackages: storeConfig.experimental.transpilePackages, @@ -55,9 +52,7 @@ const nextConfig = { sassOptions: { silenceDeprecations: ['if-function', 'legacy-js-api'], }, - // TODO: We won't need to enable this experimental feature when migrating to Next.js 13 experimental: { - instrumentationHook: true, scrollRestoration: !storeConfig.experimental.scrollRestoration, }, /* @@ -68,6 +63,27 @@ const nextConfig = { * */ outputFileTracingRoot: getRootFolder(), webpack: (config, { isServer, dev }) => { + // `@opentelemetry/sdk-node` (pulled in by @faststore/diagnostics) does an + // optional `require('@opentelemetry/shim-opencensus')` guarded by a try/catch. + // The module is an optional peer that isn't installed, so webpack reports a + // "Module not found" error it can't see is intentional. Aliasing it to `false` + // resolves it to an empty module, matching the graceful degradation the SDK + // already expects at runtime (Turbopack handles this case on its own). + config.resolve.alias = { + ...config.resolve.alias, + '@opentelemetry/shim-opencensus': false, + } + + // `require-in-the-middle` (used by OpenTelemetry instrumentation) patches + // modules through a dynamic `require` that webpack can't statically analyze, + // emitting a benign "Critical dependency" warning. `serverExternalPackages` + // handles this for Turbopack, but the webpack builder still traces into it, + // so we silence the warning for this module specifically. + config.ignoreWarnings = [ + ...(config.ignoreWarnings ?? []), + { module: /require-in-the-middle/ }, + ] + // https://github.com/vercel/next.js/discussions/11267#discussioncomment-2479112 // camel-case style names from css modules config.module.rules diff --git a/packages/core/package.json b/packages/core/package.json index 546f44bb37..939517c390 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/core", - "version": "4.5.1", + "version": "4.6.0-dev.6", "license": "MIT", "repository": { "type": "git", diff --git a/packages/core/src/components/sections/RecommendationShelf/README.md b/packages/core/src/components/sections/RecommendationShelf/README.md index 60ea3c5b87..80ab171213 100644 --- a/packages/core/src/components/sections/RecommendationShelf/README.md +++ b/packages/core/src/components/sections/RecommendationShelf/README.md @@ -99,8 +99,11 @@ All recommendation work runs client-side after hydration: 5. **Render** — shows `ProductShelfSkeleton` while loading; returns `null` on error or empty results; otherwise renders the carousel and optional heading. 6. **Tracking** — when correlation and campaign identifiers are present, the shelf emits `data-af-*` attributes for Activity Flow. PDP product views use `product:*` meta tags from `pages/[slug]/p.tsx`. +Steps 1 to 4 carry no presentation: they live in `src/sdk/recommendations` and are orchestrated by `useRecommendationShelf()`, which the shelf consumes. + Related files: +- Shared data layer: `src/sdk/recommendations/` - Shelf: `src/components/sections/RecommendationShelf/` - Hook: `src/sdk/analytics/hooks/useStartRecommendationSession.ts` - Layout: `src/Layout.tsx` @@ -129,7 +132,7 @@ import { RecommendationShelf } from 'src/components/sections/RecommendationShelf - **`ProductCard`**: custom card component (defaults to the core `ProductCard`). - **`mapProductToProductCard`**: maps each recommended product (normalized `StoreProduct`) into card props. When provided, it fully owns the card props and the default `productCardConfiguration` merge no longer applies. -Campaign types and VRN validation are defined in `vrn.ts` (`VRN_TYPE_TO_RECOMMENDATION`). Updates should be made there and mirrored in the CMS schema `pattern`. +Campaign types and VRN validation are defined in `src/sdk/recommendations/vrn.ts` (`VRN_TYPE_TO_RECOMMENDATION`). Updates should be made there and mirrored in the CMS schema `pattern`. ## Placement guidelines diff --git a/packages/core/src/components/sections/RecommendationShelf/RecommendationShelf.tsx b/packages/core/src/components/sections/RecommendationShelf/RecommendationShelf.tsx index 33375dd965..7a73cdf4da 100644 --- a/packages/core/src/components/sections/RecommendationShelf/RecommendationShelf.tsx +++ b/packages/core/src/components/sections/RecommendationShelf/RecommendationShelf.tsx @@ -1,14 +1,13 @@ import { type ComponentType, useId, useMemo } from 'react' -import { usePDP } from '@faststore/core' -import { ProductShelf, Carousel } from '@faststore/ui' +import { Carousel, ProductShelf } from '@faststore/ui' import type { ProductSummary_ProductFragment } from '@generated/graphql' import DefaultProductCard, { type ProductCardProps, } from 'src/components/product/ProductCard' import ProductShelfSkeleton from 'src/components/skeletons/ProductShelfSkeleton' -import { useCart } from 'src/sdk/cart' +import { useRecommendationShelf } from 'src/sdk/recommendations' import useScreenResize from 'src/sdk/ui/useScreenResize' import type { @@ -16,48 +15,6 @@ import type { RecommendationShelfProps, } from './RecommendationShelf.types' import styles from './section.module.scss' -import { - useRecommendations, - type RecommendationInput, -} from './useRecommendations' -import { useRecommendationUserId } from './useRecommendationUserId' -import { getTypeFromVrn } from './vrn' - -function getRecommendationArguments( - campaignVrn: string, - context: { userId?: string | null; contextProducts: string[] } -): RecommendationInput | null { - const { userId, contextProducts } = context - const type = getTypeFromVrn(campaignVrn) - - // `type` is null for malformed/unknown VRNs; bail out so we never fetch (and - // never throw) on an invalid campaign coming from the CMS. - if (!type || !userId) return null - - switch (type) { - case 'NEXT_INTERACTION': - case 'VISUAL_SIMILARITY': - case 'CROSS_SELL': - case 'SIMILAR_ITEMS': - // These campaigns need product context. Without any (e.g. `CART` context - // on an empty cart, or `PDP` context outside a product page), skip the - // fetch instead of requesting recommendations we can't anchor. - if (contextProducts.length === 0) { - return null - } - return { - userId, - campaignVrn, - products: contextProducts, - } - default: - return { - userId, - campaignVrn, - products: [], - } - } -} export function RecommendationShelf< TCardProps extends object = ProductCardProps, @@ -96,43 +53,14 @@ export function RecommendationShelf< const itemsPerPage = isMobile || isTablet ? itemsPerPageMobile : itemsPerPageDesktop - const userId = useRecommendationUserId(campaignVrn) - - const { data: productDetailPage } = usePDP() - const { items: cartItems } = useCart() - - // Resolve the products used as context for the request from the configured - // source: the current PDP product, or the (deduplicated) cart items. - const contextProducts = useMemo(() => { - if (itemsContext === 'CART') { - return Array.from( - new Set( - cartItems - .map((item) => item.itemOffered.isVariantOf.productGroupID) - .filter(Boolean) - ) - ) - } - - const pdpProduct = productDetailPage?.product?.isVariantOf?.productGroupID - - return pdpProduct ? [pdpProduct] : [] - }, [itemsContext, cartItems, productDetailPage]) - - // Gate the whole feature behind the CMS opt-in: when Recommendations is - // disabled the shelf never requests recommendations (nor renders). - const recommendationArgs = enableRecommendations - ? getRecommendationArguments(campaignVrn, { - userId, - contextProducts, - }) - : null - - const { data, isLoading, error } = useRecommendations(recommendationArgs) + const { items, isLoading, error, campaign, correlationId } = + useRecommendationShelf({ + campaignVrn, + enableRecommendations, + itemsContext, + }) - const items = data?.products || [] - const correlationId = data?.correlationId - const campaignId = data?.campaign.id + const campaignId = campaign?.id const productIds = useMemo( () => items.map((p) => p.isVariantOf.productGroupID).join(', '), @@ -147,15 +75,6 @@ export function RecommendationShelf< ) if (error) { - // Don't log `recommendationArgs`: it carries the userId. Log only - // non-identifying context. - console.error( - 'Error fetching recommendations', - error.cause, - error.message, - 'for campaign', - campaignVrn - ) return null } @@ -178,9 +97,9 @@ export function RecommendationShelf< : {})} > - {(title || data?.campaign.title) && ( + {(title || campaign?.title) && (

- {title || data?.campaign.title} + {title || campaign?.title}

)} diff --git a/packages/core/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts b/packages/core/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts index 36de315cc7..2af27ad440 100644 --- a/packages/core/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts +++ b/packages/core/src/components/sections/RecommendationShelf/RecommendationShelf.types.ts @@ -4,6 +4,9 @@ import type { CarouselProps } from '@faststore/ui' import type { ProductSummary_ProductFragment } from '@generated/graphql' import type { ProductCardProps } from 'src/components/product/ProductCard' +import type { ItemContext } from 'src/sdk/recommendations' + +export type { ItemContext, RecommendationType } from 'src/sdk/recommendations' /** * Maps a recommendation product (a normalized `StoreProduct`, identical to the @@ -17,14 +20,6 @@ export type RecommendationProductCardMapper = ( index: number ) => TCardProps -/** - * Source of the products used as context for the recommendation request: - * - `'PDP'`: the current product detail page product. - * - `'CART'`: the products currently in the cart (useful for cross-sell on the - * cart page). - */ -export type ItemContext = 'PDP' | 'CART' - export type RecommendationShelfProps< TCardProps extends object = ProductCardProps, > = { @@ -106,13 +101,3 @@ export type RecommendationShelfProps< readonly bordered?: boolean } } - -export type RecommendationType = - | 'CROSS_SELL' - | 'SIMILAR_ITEMS' - | 'PERSONALIZED' - | 'TOP_ITEMS' - | 'LAST_SEEN' - | 'SEARCH_BASED' - | 'VISUAL_SIMILARITY' - | 'NEXT_INTERACTION' diff --git a/packages/core/src/instrumentation.ts b/packages/core/src/instrumentation.ts index 5ad9e872ba..61ea93fa1f 100644 --- a/packages/core/src/instrumentation.ts +++ b/packages/core/src/instrumentation.ts @@ -1,17 +1,25 @@ import config from '../discovery.config' import pkgJSON from '../package.json' +/** + * Next.js instrumentation hook. This is the only place telemetry is booted, so + * the OTel SDK is registered before any request is served. + */ export async function register() { - if ( - process.env.NEXT_RUNTIME === 'nodejs' && - config.analytics.otelEnabled === true - ) { + // Disabled feature until release date + const instrumentationEnabled = false + + // The runtime check must wrap the import instead of guarding with an early + // return: bundlers replace `process.env.NEXT_RUNTIME` with a literal, so only + // a positive branch gets dropped from the edge bundle. Left reachable, the + // OTel SDK pulls in Node builtins (`fs`, `zlib`, `stream`) that fail to resolve. + if (process.env.NEXT_RUNTIME === 'nodejs' && instrumentationEnabled) { const { name, version } = pkgJSON + try { const { getTelemetryClient } = await import('@faststore/diagnostics') - console.log('Instrumemtation.ts: Getting telemetry client') - return getTelemetryClient({ + return await getTelemetryClient({ serviceName: config.analytics?.serviceName ?? name, version, account: config.api.storeId, @@ -19,7 +27,7 @@ export async function register() { packageName: name, }) } catch (error) { - console.error('Failed to initialize OTEL Instrumentation') + console.error('Failed to initialize OTEL Instrumentation', error) } } } diff --git a/packages/core/src/pages/[...slug].tsx b/packages/core/src/pages/[...slug].tsx index f60484bc61..c42caff409 100644 --- a/packages/core/src/pages/[...slug].tsx +++ b/packages/core/src/pages/[...slug].tsx @@ -191,16 +191,6 @@ export const getStaticProps: GetStaticProps< globalSectionsFooterPromise, ]) - const [serverManyProductsData, serverManyProductsVariables] = - await fetchServerManyProducts({ - itemsPerPage: cmsPage?.settings?.productGallery?.itemsPerPage, - sort: cmsPage?.settings?.productGallery - ?.sortBySelection as SearchState['sort'], - term: '', - selectedFacets: data?.collection?.meta.selectedFacets, - locale, - }) - const notFound = errors.find(isNotFoundError) if (notFound) { @@ -216,6 +206,7 @@ export const getStaticProps: GetStaticProps< return { notFound: true, + revalidate: storeConfig.experimental.revalidate404 ?? 60 * 5, // 5 minutes } } @@ -224,6 +215,16 @@ export const getStaticProps: GetStaticProps< throw errors[0] } + const [serverManyProductsData, serverManyProductsVariables] = + await fetchServerManyProducts({ + itemsPerPage: cmsPage?.settings?.productGallery?.itemsPerPage, + sort: cmsPage?.settings?.productGallery + ?.sortBySelection as SearchState['sort'], + term: '', + selectedFacets: data?.collection?.meta.selectedFacets, + locale, + }) + const globalSectionsResult = injectGlobalSections({ globalSections, globalSectionsHeader, diff --git a/packages/core/src/pages/[slug]/p.tsx b/packages/core/src/pages/[slug]/p.tsx index e9cb6d636f..01555f64da 100644 --- a/packages/core/src/pages/[slug]/p.tsx +++ b/packages/core/src/pages/[slug]/p.tsx @@ -47,6 +47,7 @@ import type { PreviewData } from 'src/server/content/types' type StoreConfig = typeof storeConfig & { experimental: { revalidate?: number + revalidate404?: number enableClientOffer?: boolean } } @@ -482,6 +483,8 @@ export const getStaticProps: GetStaticProps< return { notFound: true, + revalidate: + (storeConfig as StoreConfig).experimental.revalidate404 ?? 60 * 5, // 5 minutes } } diff --git a/packages/core/src/pages/api/graphql.ts b/packages/core/src/pages/api/graphql.ts index 394e491a8f..1f3145ef48 100644 --- a/packages/core/src/pages/api/graphql.ts +++ b/packages/core/src/pages/api/graphql.ts @@ -13,6 +13,9 @@ import { getRequestHostname } from 'src/utils/getRequestHostname' import { isLocalHost } from 'src/utils/isLocalHost' import { shouldForceRefreshTokenForValidateSession } from 'src/utils/validateSessionRefreshToken' import { execute } from '../../server' +import { logger } from '@faststore/diagnostics' + +const OTELLogger = logger('@faststore/core') const DEFAULT_MAX_AGE = 5 * 60 // 5 minutes const DEFAULT_STALE_WHILE_REVALIDATE = 60 * 60 // 1 hour @@ -150,6 +153,12 @@ const hasVtexIdclientAutCookie = (request: NextApiRequest): boolean => { const handler: NextApiHandler = async (request, response) => { if (request.method !== 'POST' && request.method !== 'GET') { response.status(405).end() + OTELLogger( + 'info', + 'Invalid request method: %s %s', + request.method, + request.url + ) return } @@ -158,6 +167,15 @@ const handler: NextApiHandler = async (request, response) => { // value is used to cache bust the request if there is a VtexIdclientAutCookie const { operation, variables, query, v: value } = parseRequest(request) + // Variables and the query body are deliberately not logged: they carry + // session and customer data. + OTELLogger( + 'debug', + 'operation: %s hash: %s', + operation?.__meta__?.operationName, + operation?.__meta__?.operationHash + ) + const isLocal = isLocalHost(getRequestHostname(request.headers.host)) if ( @@ -209,18 +227,21 @@ const handler: NextApiHandler = async (request, response) => { // upstream status) is nested in `originalError`. Recover it so the BFF // propagates the real status instead of collapsing everything to 500. const fastStoreError = errors.map(recoverFastStoreError).find(Boolean) + const reportedErrors = errors.map((graphqlError) => { + const fsError = recoverFastStoreError(graphqlError) + + return { + message: (graphqlError as { message?: string })?.message, + status: fsError?.extensions.status, + type: fsError?.extensions.type, + } + }) - console.error( - 'Graphql execution returned with error: ', - errors.map((graphqlError) => { - const fsError = recoverFastStoreError(graphqlError) - - return { - message: (graphqlError as { message?: string })?.message, - status: fsError?.extensions.status, - type: fsError?.extensions.type, - } - }) + console.error('Graphql execution returned with error: ', reportedErrors) + OTELLogger( + 'error', + 'Graphql execution returned with error: %o', + reportedErrors ) const status = fastStoreError?.extensions.status ?? 500 @@ -304,6 +325,11 @@ const handler: NextApiHandler = async (request, response) => { 'Something unexpected occurred querying Graphql endpoint: \n', err ) + OTELLogger( + 'error', + 'Something unexpected occurred querying Graphql endpoint: %o', + err + ) if (err instanceof BadRequestError) { response.status(400).end() diff --git a/packages/core/src/sdk/cart/index.ts b/packages/core/src/sdk/cart/index.ts index c23939cc3d..797da04a2e 100644 --- a/packages/core/src/sdk/cart/index.ts +++ b/packages/core/src/sdk/cart/index.ts @@ -13,8 +13,10 @@ import type { import storeConfig from '../../../discovery.config' import { request } from '../graphql/request' -import { sessionStore } from '../session' +import { hasValidatedSessionStore, sessionStore } from '../session' +import { syncSalesChannelFromOrderForm } from '../session/syncSalesChannelFromOrderForm' import { createValidationStore, useStore } from '../useStore' +import { waitForSessionValidated } from './waitForSessionValidated' export interface CartItem extends SDKCartItem, @@ -33,6 +35,7 @@ export const ValidateCartMutation = gql(` validateCart(cart: $cart, session: $session) { order { orderNumber + salesChannel acceptedOffer { ...CartItem } @@ -114,6 +117,8 @@ const getItemId = (item: Pick) => .join('::') const validateCart = async (cart: Cart): Promise => { + await waitForSessionValidated(hasValidatedSessionStore) + const { validateCart: validated = null } = await request< ValidateCartMutationMutation, ValidateCartMutationMutationVariables @@ -151,6 +156,15 @@ const validateCart = async (cart: Cart): Promise => { }, }) + const adoptedSalesChannel = validated?.order?.salesChannel + if (adoptedSalesChannel) { + syncSalesChannelFromOrderForm( + adoptedSalesChannel, + () => sessionStore.read(), + (session) => sessionStore.setSilent(session) + ) + } + return ( validated && { id: validated.order.orderNumber, diff --git a/packages/core/src/sdk/cart/waitForSessionValidated.ts b/packages/core/src/sdk/cart/waitForSessionValidated.ts new file mode 100644 index 0000000000..338ddb5b77 --- /dev/null +++ b/packages/core/src/sdk/cart/waitForSessionValidated.ts @@ -0,0 +1,43 @@ +import type { Store } from '@faststore/sdk' + +/** Max time to wait for the first validateSession before validating the cart. */ +export const SESSION_VALIDATION_WAIT_MS = 3_000 + +/** + * Defers cart validation until session has been validated at least once. + * This reduces first-paint races between Session and Cart; sales-channel + * correctness after external flows (e.g. Quick Order) is enforced in the API + * by preserving/adopting the orderForm SC. + */ +export const waitForSessionValidated = async ( + hasValidatedSessionStore: Store, + waitMs = SESSION_VALIDATION_WAIT_MS +): Promise => { + if (hasValidatedSessionStore.read()) { + return + } + + await new Promise((resolve) => { + const timeoutId = setTimeout(() => { + unsubscribe() + resolve() + }, waitMs) + + const unsubscribe = hasValidatedSessionStore.subscribe((validated) => { + if (!validated) { + return + } + + clearTimeout(timeoutId) + unsubscribe() + resolve() + }) + + // Race: validation may have finished between read() and subscribe() + if (hasValidatedSessionStore.read()) { + clearTimeout(timeoutId) + unsubscribe() + resolve() + } + }) +} diff --git a/packages/core/src/sdk/recommendations/index.ts b/packages/core/src/sdk/recommendations/index.ts new file mode 100644 index 0000000000..f06f7eb172 --- /dev/null +++ b/packages/core/src/sdk/recommendations/index.ts @@ -0,0 +1,17 @@ +export type { ItemContext, RecommendationType } from './types' +export type { + RecommendationInput, + RecommendationProduct, + RecommendationResponse, +} from './useRecommendations' +export { useRecommendations } from './useRecommendations' +export { useRecommendationUserId } from './useRecommendationUserId' +export type { + UseRecommendationShelfArgs, + UseRecommendationShelfResult, +} from './useRecommendationShelf' +export { + getRecommendationArguments, + useRecommendationShelf, +} from './useRecommendationShelf' +export { getTypeFromVrn, isValidVrn } from './vrn' diff --git a/packages/core/src/sdk/recommendations/types.ts b/packages/core/src/sdk/recommendations/types.ts new file mode 100644 index 0000000000..36c3ed724a --- /dev/null +++ b/packages/core/src/sdk/recommendations/types.ts @@ -0,0 +1,17 @@ +/** + * Source of the products used as context for the recommendation request: + * - `'PDP'`: the current product detail page product. + * - `'CART'`: the products currently in the cart (useful for cross-sell on the + * cart page and inside the cart drawer). + */ +export type ItemContext = 'PDP' | 'CART' + +export type RecommendationType = + | 'CROSS_SELL' + | 'SIMILAR_ITEMS' + | 'PERSONALIZED' + | 'TOP_ITEMS' + | 'LAST_SEEN' + | 'SEARCH_BASED' + | 'VISUAL_SIMILARITY' + | 'NEXT_INTERACTION' diff --git a/packages/core/src/sdk/recommendations/useRecommendationShelf.ts b/packages/core/src/sdk/recommendations/useRecommendationShelf.ts new file mode 100644 index 0000000000..e11c9cceeb --- /dev/null +++ b/packages/core/src/sdk/recommendations/useRecommendationShelf.ts @@ -0,0 +1,141 @@ +import { usePDP } from '@faststore/core' +import { useMemo } from 'react' + +import { useCart } from 'src/sdk/cart' + +import type { ItemContext } from './types' +import { + type RecommendationInput, + type RecommendationResponse, + useRecommendations, +} from './useRecommendations' +import { useRecommendationUserId } from './useRecommendationUserId' +import { getTypeFromVrn } from './vrn' + +export function getRecommendationArguments( + campaignVrn: string, + context: { userId?: string | null; contextProducts: string[] } +): RecommendationInput | null { + const { userId, contextProducts } = context + const type = getTypeFromVrn(campaignVrn) + + // `type` is null for malformed/unknown VRNs; bail out so we never fetch (and + // never throw) on an invalid campaign coming from the CMS. + if (!type || !userId) return null + + switch (type) { + case 'NEXT_INTERACTION': + case 'VISUAL_SIMILARITY': + case 'CROSS_SELL': + case 'SIMILAR_ITEMS': + // These campaigns need product context. Without any (e.g. `CART` context + // on an empty cart, or `PDP` context outside a product page), skip the + // fetch instead of requesting recommendations we can't anchor. + if (contextProducts.length === 0) { + return null + } + return { + userId, + campaignVrn, + products: contextProducts, + } + default: + return { + userId, + campaignVrn, + products: [], + } + } +} + +export type UseRecommendationShelfArgs = { + readonly campaignVrn: string + /** + * Opt-in for VTEX Recommendations. When `false` (default) the campaign is + * never requested. The anonymous user id is still resolved, but it goes + * unused and never leaves the browser. + */ + readonly enableRecommendations?: boolean + /** + * Where to read the products used as context for the recommendation request. + * @default 'PDP' + */ + readonly itemsContext?: ItemContext +} + +export type UseRecommendationShelfResult = { + readonly items: RecommendationResponse['products'] + readonly isLoading: boolean + readonly error: unknown + readonly campaign?: RecommendationResponse['campaign'] + readonly correlationId?: string +} + +const EMPTY_ITEMS: RecommendationResponse['products'] = [] + +/** + * Headless data layer behind every recommendation shelf: resolves the campaign + * type, the anonymous user id and the product context, then fetches the + * campaign. It renders nothing — presentation lives in the shelf components + * that consume it, so a new surface can reuse the campaign rules instead of + * reimplementing them. + */ +export function useRecommendationShelf({ + campaignVrn, + enableRecommendations = false, + itemsContext = 'PDP', +}: UseRecommendationShelfArgs): UseRecommendationShelfResult { + const userId = useRecommendationUserId(campaignVrn) + + const { data: productDetailPage } = usePDP() + const { items: cartItems } = useCart() + + // Resolve the products used as context for the request from the configured + // source: the current PDP product, or the (deduplicated) cart items. + const contextProducts = useMemo(() => { + if (itemsContext === 'CART') { + return Array.from( + new Set( + cartItems + .map((item) => item.itemOffered.isVariantOf.productGroupID) + .filter(Boolean) + ) + ) + } + + const pdpProduct = productDetailPage?.product?.isVariantOf?.productGroupID + + return pdpProduct ? [pdpProduct] : [] + }, [itemsContext, cartItems, productDetailPage]) + + // Gate the whole feature behind the CMS opt-in: when Recommendations is + // disabled the shelf never requests recommendations (nor renders). + const recommendationArgs = enableRecommendations + ? getRecommendationArguments(campaignVrn, { + userId, + contextProducts, + }) + : null + + const { data, isLoading, error } = useRecommendations(recommendationArgs) + + if (error) { + // Don't log `recommendationArgs`: it carries the userId. Log only + // non-identifying context. + console.error( + 'Error fetching recommendations', + (error as { cause?: unknown })?.cause, + (error as { message?: string })?.message, + 'for campaign', + campaignVrn + ) + } + + return { + items: error ? EMPTY_ITEMS : (data?.products ?? EMPTY_ITEMS), + isLoading, + error, + campaign: data?.campaign, + correlationId: data?.correlationId, + } +} diff --git a/packages/core/src/components/sections/RecommendationShelf/useRecommendationUserId.ts b/packages/core/src/sdk/recommendations/useRecommendationUserId.ts similarity index 100% rename from packages/core/src/components/sections/RecommendationShelf/useRecommendationUserId.ts rename to packages/core/src/sdk/recommendations/useRecommendationUserId.ts diff --git a/packages/core/src/components/sections/RecommendationShelf/useRecommendations.ts b/packages/core/src/sdk/recommendations/useRecommendations.ts similarity index 100% rename from packages/core/src/components/sections/RecommendationShelf/useRecommendations.ts rename to packages/core/src/sdk/recommendations/useRecommendations.ts diff --git a/packages/core/src/components/sections/RecommendationShelf/vrn.ts b/packages/core/src/sdk/recommendations/vrn.ts similarity index 96% rename from packages/core/src/components/sections/RecommendationShelf/vrn.ts rename to packages/core/src/sdk/recommendations/vrn.ts index 263dfbbb46..c3ca29d797 100644 --- a/packages/core/src/components/sections/RecommendationShelf/vrn.ts +++ b/packages/core/src/sdk/recommendations/vrn.ts @@ -1,4 +1,4 @@ -import type { RecommendationType } from './RecommendationShelf.types' +import type { RecommendationType } from './types' // Single source of truth for the campaign taxonomy: maps each Intelligent Search // campaign VRN type to its `RecommendationType`. The supported VRN types, the diff --git a/packages/core/src/sdk/session/index.ts b/packages/core/src/sdk/session/index.ts index 8a1fa0ef82..72568346a3 100644 --- a/packages/core/src/sdk/session/index.ts +++ b/packages/core/src/sdk/session/index.ts @@ -194,6 +194,9 @@ export const validateSession = async (session: Session) => { const [validationStore, onValidate, hasValidatedStore] = createValidationStore(validateSession) +/** True after at least one validateSession cycle finished (success or failure). */ +export const hasValidatedSessionStore = hasValidatedStore + const urlAwareInitialSession = getInitialSession() const defaultStore = createSessionStore( urlAwareInitialSession, @@ -214,6 +217,17 @@ export const sessionStore = { // Trigger cart revalidation when session changes cartStore.set(cartStore.read()) }, + /** + * Updates session without revalidating the cart. Used when aligning + * `channel.salesChannel` to an orderForm SC adopted by `validateCart`. + * Still runs `validateSession` via the store's optimistic validator; that + * path keeps an explicit (non-default) client SC. + */ + setSilent: (val: Session) => { + if (deepEqual(val, defaultStore.read()) === true) return + + defaultStore.set(val) + }, } export async function logoutAndClearSession(session: Session) { diff --git a/packages/core/src/sdk/session/syncSalesChannelFromOrderForm.ts b/packages/core/src/sdk/session/syncSalesChannelFromOrderForm.ts new file mode 100644 index 0000000000..ba86ec9fad --- /dev/null +++ b/packages/core/src/sdk/session/syncSalesChannelFromOrderForm.ts @@ -0,0 +1,68 @@ +import type { Session } from '@faststore/sdk' + +function parseChannelRecord( + raw: string | null | undefined +): Record { + try { + const parsed: unknown = JSON.parse(raw || '{}') + + if ( + parsed !== null && + typeof parsed === 'object' && + !Array.isArray(parsed) + ) { + return parsed as Record + } + } catch { + // Malformed channel — reset below. + } + + return {} +} + +function readSalesChannel(channel: Record): string { + const value = channel.salesChannel + + if (typeof value === 'string' || typeof value === 'number') { + return String(value) + } + + return '' +} + +/** + * Aligns `fs::session.channel` with a sales channel adopted from the orderForm + * (e.g. after Quick Order) without revalidating the cart. + * + * Uses `sessionStore.setSilent` so we do not trigger `cartStore.set` mid-flight + * (that race previously wiped SC-exclusive items). Marks the channel as + * explicit (`hasOnlyDefaultSalesChannel: false`) so `validateSession` keeps it. + */ +export function syncSalesChannelFromOrderForm( + salesChannel: string | null | undefined, + readSession: () => Session, + setSilent: (session: Session) => void +): boolean { + if (salesChannel == null || salesChannel === '') { + return false + } + + const session = readSession() + const channel = parseChannelRecord(session.channel) + const nextSalesChannel = String(salesChannel) + + if (readSalesChannel(channel) === nextSalesChannel) { + return false + } + + setSilent({ + ...session, + channel: JSON.stringify({ + ...channel, + salesChannel: nextSalesChannel, + hasOnlyDefaultSalesChannel: false, + }), + }) + + return true +} diff --git a/packages/core/src/server/index.ts b/packages/core/src/server/index.ts index 934927544a..bb93620897 100644 --- a/packages/core/src/server/index.ts +++ b/packages/core/src/server/index.ts @@ -16,6 +16,7 @@ import { GraphqlVtexSchema, isFastStoreError, } from '@faststore/api' +import { OTELAPI } from '@faststore/diagnostics' import { loadFilesSync } from '@graphql-tools/load-files' import { mergeTypeDefs } from '@graphql-tools/merge' import { makeExecutableSchema } from '@graphql-tools/schema' @@ -28,7 +29,7 @@ import thirdPartyResolvers from '../customizations/src/graphql/thirdParty/resolv import vtexExtensionsResolvers from '../customizations/src/graphql/vtex/resolvers' import type { Operation } from '../sdk/graphql/request' -import { apiOptions, withTraceClient } from './options' +import { apiOptions } from './options' interface ExecuteOptions> { operation: Operation @@ -70,8 +71,7 @@ export function getFinalAPISchema() { } export const getEnvelop = async () => { - const options = await withTraceClient(apiOptions) - const apiContextFactory = await GraphqlVtexContextFactory(options) + const apiContextFactory = await GraphqlVtexContextFactory(apiOptions) return envelop({ plugins: [ @@ -136,13 +136,17 @@ export const execute = async , D>( contextValue.storage.locale = locale } - const { data, errors } = (await run({ - schema, - document: parse(query), - variableValues: variables, - contextValue, - operationName, - })) as { data: D; errors: unknown[] } + const { data, errors } = await withRootSpan( + `graphql ${operationName ?? 'operation'}`, + () => + run({ + schema, + document: parse(query), + variableValues: variables, + contextValue, + operationName, + }) as Promise<{ data: D; errors: unknown[] }> + ) return { data, @@ -153,3 +157,27 @@ export const execute = async , D>( }, } } + +/** + * Runs an operation inside a per-request root span, made the active context so + * the resolver spans emitted by `@faststore/api` parent to it. Resolves to a + * plain call when telemetry is disabled. + */ +async function withRootSpan(name: string, run: () => Promise) { + if (!apiOptions.OTEL_ENABLED) { + return run() + } + + const span = OTELAPI.trace.getTracer('@faststore/core').startSpan(name) + const otelContext = OTELAPI.trace.setSpan(OTELAPI.context.active(), span) + + try { + return await OTELAPI.context.with(otelContext, run) + } catch (error) { + span.setStatus({ code: OTELAPI.SpanStatusCode.ERROR }) + span.recordException(error as Error) + throw error + } finally { + span.end() + } +} diff --git a/packages/core/src/server/options.ts b/packages/core/src/server/options.ts index 99937c9e5e..9e694e49aa 100644 --- a/packages/core/src/server/options.ts +++ b/packages/core/src/server/options.ts @@ -1,9 +1,8 @@ import type { APIOptions } from '@faststore/api' -import { getTraceClient } from '@faststore/diagnostics' import storeConfig from '../../discovery.config' import pkgJSON from '../../package.json' -const { name, version } = pkgJSON +const { version } = pkgJSON export const apiOptions: APIOptions = { platform: storeConfig.platform as APIOptions['platform'], @@ -23,25 +22,7 @@ export const apiOptions: APIOptions = { storeConfig.api?.enableUnavailableItemsOnCart ?? false, }, version, - OTEL: { - enabled: storeConfig.analytics.otelEnabled, - }, + // feature flag default to false as it should not emit traces/logs yet + OTEL_ENABLED: false, discoveryConfig: storeConfig, } - -export async function withTraceClient( - apiOptions: T -): Promise { - const OTEL = {} - getTraceClient( - apiOptions?.discoveryConfig?.analytics?.serviceName ?? name - )?.inject(OTEL) - - return { - ...apiOptions, - OTEL: { - ...OTEL, - enabled: storeConfig.analytics?.otelEnabled?.toString() === 'true', - }, - } as T -} diff --git a/packages/core/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx b/packages/core/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx index f8950e9e11..8a0b8d6b85 100644 --- a/packages/core/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx +++ b/packages/core/test/components/RecommendationShelf/RecommendationShelf.browser.test.tsx @@ -29,23 +29,17 @@ vi.mock('src/components/skeletons/ProductShelfSkeleton', () => ({ })) const useRecommendations = vi.hoisted(() => vi.fn()) -vi.mock( - 'src/components/sections/RecommendationShelf/useRecommendations', - () => ({ - useRecommendations, - }) -) +vi.mock('src/sdk/recommendations/useRecommendations', () => ({ + useRecommendations, +})) const useScreenResize = vi.hoisted(() => vi.fn()) vi.mock('src/sdk/ui/useScreenResize', () => ({ default: useScreenResize })) const useRecommendationUserId = vi.hoisted(() => vi.fn()) -vi.mock( - 'src/components/sections/RecommendationShelf/useRecommendationUserId', - () => ({ - useRecommendationUserId, - }) -) +vi.mock('src/sdk/recommendations/useRecommendationUserId', () => ({ + useRecommendationUserId, +})) import { RecommendationShelf } from 'src/components/sections/RecommendationShelf/RecommendationShelf' diff --git a/packages/core/test/instrumentation.test.ts b/packages/core/test/instrumentation.test.ts new file mode 100644 index 0000000000..97262e8a96 --- /dev/null +++ b/packages/core/test/instrumentation.test.ts @@ -0,0 +1,27 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { register } from '../src/instrumentation' + +const getTelemetryClient = vi.hoisted(() => vi.fn()) + +vi.mock('@faststore/diagnostics', () => ({ getTelemetryClient })) + +beforeEach(() => { + vi.clearAllMocks() +}) + +afterEach(() => { + vi.unstubAllEnvs() +}) + +describe('register', () => { + it.each(['nodejs', 'edge', undefined])( + 'does not boot telemetry on the %s runtime while the feature is off', + async (runtime) => { + vi.stubEnv('NEXT_RUNTIME', runtime) + + await expect(register()).resolves.toBeUndefined() + expect(getTelemetryClient).not.toHaveBeenCalled() + } + ) +}) diff --git a/packages/core/test/pages/api/graphql.test.ts b/packages/core/test/pages/api/graphql.test.ts index 9d7a8cc6e5..3b32a236c7 100644 --- a/packages/core/test/pages/api/graphql.test.ts +++ b/packages/core/test/pages/api/graphql.test.ts @@ -29,9 +29,13 @@ const createResponse = () => { return res as unknown as NextApiResponse & typeof res } -const createRequest = (operationName = 'ClientShippingSimulationQuery') => +const createRequest = ( + operationName = 'ClientShippingSimulationQuery', + method = 'GET' +) => ({ - method: 'GET', + method, + url: '/api/graphql', headers: { host: 'localhost:3000' }, query: { operationName, @@ -165,3 +169,37 @@ describe('/api/graphql error status propagation', () => { expect(res.status).toHaveBeenCalledWith(404) }) }) + +describe('/api/graphql request handling', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it.each(['PUT', 'DELETE', 'PATCH', 'OPTIONS'])( + 'rejects %s with 405 without reaching the GraphQL layer', + async (method) => { + const res = createResponse() + await handler(createRequest(undefined, method), res) + + expect(res.status).toHaveBeenCalledWith(405) + expect(mockedExecute).not.toHaveBeenCalled() + } + ) + + it.each([ + ['BadRequestError', () => new BadRequestError('malformed'), 400], + ['UnauthorizedError', () => new UnauthorizedError('expired'), 401], + ['an unexpected failure', () => new Error('socket hang up'), 500], + ])( + 'maps %s thrown by execute to its status', + async (_name, makeError, status) => { + mockedExecute.mockRejectedValue(makeError()) + + const res = createResponse() + await handler(createRequest(), res) + + expect(res.status).toHaveBeenCalledWith(status) + expect(res.end).toHaveBeenCalled() + } + ) +}) diff --git a/packages/core/test/sdk/cart/waitForSessionValidated.test.ts b/packages/core/test/sdk/cart/waitForSessionValidated.test.ts new file mode 100644 index 0000000000..e4a57b39c7 --- /dev/null +++ b/packages/core/test/sdk/cart/waitForSessionValidated.test.ts @@ -0,0 +1,96 @@ +import { createBaseStore } from '@faststore/sdk' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { + SESSION_VALIDATION_WAIT_MS, + waitForSessionValidated, +} from '../../../src/sdk/cart/waitForSessionValidated' + +describe('waitForSessionValidated', () => { + afterEach(() => { + vi.useRealTimers() + }) + + it('resolves immediately when session was already validated', async () => { + const store = createBaseStore(true) + + await expect(waitForSessionValidated(store)).resolves.toBeUndefined() + }) + + it('resolves when session becomes validated after subscribe', async () => { + const store = createBaseStore(false) + + const pending = waitForSessionValidated(store, 5_000) + store.set(true) + + await expect(pending).resolves.toBeUndefined() + }) + + it('ignores subscribe callbacks while validated is still false', async () => { + vi.useFakeTimers() + const listeners: Array<(value: boolean) => void> = [] + const store = { + read: vi.fn(() => false), + readInitial: vi.fn(() => false), + subscribe: vi.fn((listener: (value: boolean) => void) => { + listeners.push(listener) + return () => { + const index = listeners.indexOf(listener) + if (index >= 0) { + listeners.splice(index, 1) + } + } + }), + set: vi.fn(), + } + + const pending = waitForSessionValidated(store, SESSION_VALIDATION_WAIT_MS) + + // Emit false — should not resolve yet + for (const listener of listeners) { + listener(false) + } + + await vi.advanceTimersByTimeAsync(SESSION_VALIDATION_WAIT_MS) + await expect(pending).resolves.toBeUndefined() + }) + + it('resolves on timeout when session never validates', async () => { + vi.useFakeTimers() + const store = createBaseStore(false) + + const pending = waitForSessionValidated(store, SESSION_VALIDATION_WAIT_MS) + + await vi.advanceTimersByTimeAsync(SESSION_VALIDATION_WAIT_MS) + + await expect(pending).resolves.toBeUndefined() + }) + + it('resolves the race when validation finishes between read and subscribe', async () => { + let current = false + const listeners: Array<(value: boolean) => void> = [] + + const store = { + read: vi.fn(() => { + // First read: not validated. Second read (post-subscribe): validated. + const value = current + current = true + return value + }), + readInitial: vi.fn(() => false), + subscribe: vi.fn((listener: (value: boolean) => void) => { + listeners.push(listener) + return () => { + const index = listeners.indexOf(listener) + if (index >= 0) { + listeners.splice(index, 1) + } + } + }), + set: vi.fn(), + } + + await expect(waitForSessionValidated(store, 5_000)).resolves.toBeUndefined() + expect(store.subscribe).toHaveBeenCalledTimes(1) + expect(store.read).toHaveBeenCalledTimes(2) + }) +}) diff --git a/packages/core/test/sdk/recommendations/useRecommendationShelf.browser.test.tsx b/packages/core/test/sdk/recommendations/useRecommendationShelf.browser.test.tsx new file mode 100644 index 0000000000..53817f3185 --- /dev/null +++ b/packages/core/test/sdk/recommendations/useRecommendationShelf.browser.test.tsx @@ -0,0 +1,199 @@ +import { renderHook } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const usePDP = vi.hoisted(() => vi.fn()) +vi.mock('@faststore/core', () => ({ usePDP })) + +const useCart = vi.hoisted(() => vi.fn()) +vi.mock('src/sdk/cart', () => ({ useCart })) + +const useRecommendations = vi.hoisted(() => vi.fn()) +vi.mock('src/sdk/recommendations/useRecommendations', () => ({ + useRecommendations, +})) + +const useRecommendationUserId = vi.hoisted(() => vi.fn()) +vi.mock('src/sdk/recommendations/useRecommendationUserId', () => ({ + useRecommendationUserId, +})) + +import { useRecommendationShelf } from 'src/sdk/recommendations/useRecommendationShelf' + +const TOP_ITEMS_VRN = 'vrn:recommendations:acc:rec-top-items-v2:campaign-1' +const CROSS_SELL_VRN = 'vrn:recommendations:acc:rec-cross-v2:campaign-1' + +const cartItem = (productGroupID: string) => ({ + itemOffered: { isVariantOf: { productGroupID } }, +}) + +const campaignData = { + products: [ + { id: 'p-1', isVariantOf: { productGroupID: 'p-1' } }, + { id: 'p-2', isVariantOf: { productGroupID: 'p-2' } }, + ], + correlationId: 'corr-1', + campaign: { id: 'camp-1', title: 'Recommended for you', type: 'TOP_ITEMS' }, +} + +const lastArgs = () => useRecommendations.mock.calls.at(-1)?.[0] + +beforeEach(() => { + usePDP.mockReturnValue({ data: undefined }) + useCart.mockReturnValue({ items: [] }) + useRecommendationUserId.mockReturnValue('user-1') + useRecommendations.mockReturnValue({ + data: campaignData, + isLoading: false, + error: null, + }) +}) + +afterEach(() => { + vi.clearAllMocks() +}) + +describe('useRecommendationShelf', () => { + it('exposes the campaign payload when enabled', () => { + const { result } = renderHook(() => + useRecommendationShelf({ + campaignVrn: TOP_ITEMS_VRN, + enableRecommendations: true, + }) + ) + + expect(result.current.items).toHaveLength(2) + expect(result.current.campaign?.title).toBe('Recommended for you') + expect(result.current.correlationId).toBe('corr-1') + }) + + it('never fetches while recommendations are disabled', () => { + renderHook(() => useRecommendationShelf({ campaignVrn: TOP_ITEMS_VRN })) + + expect(lastArgs()).toBeNull() + }) + + it('never fetches before a userId resolves', () => { + useRecommendationUserId.mockReturnValue(null) + + renderHook(() => + useRecommendationShelf({ + campaignVrn: TOP_ITEMS_VRN, + enableRecommendations: true, + }) + ) + + expect(lastArgs()).toBeNull() + }) + + it('never fetches on a malformed campaign vrn', () => { + renderHook(() => + useRecommendationShelf({ + campaignVrn: 'not-a-vrn', + enableRecommendations: true, + }) + ) + + expect(lastArgs()).toBeNull() + }) + + it('anchors cart-context campaigns on the deduplicated cart products', () => { + useCart.mockReturnValue({ + items: [cartItem('pg-1'), cartItem('pg-2'), cartItem('pg-1')], + }) + + renderHook(() => + useRecommendationShelf({ + campaignVrn: CROSS_SELL_VRN, + enableRecommendations: true, + itemsContext: 'CART', + }) + ) + + expect(lastArgs()).toEqual({ + userId: 'user-1', + campaignVrn: CROSS_SELL_VRN, + products: ['pg-1', 'pg-2'], + }) + }) + + it('skips cart-context campaigns while the cart is empty', () => { + renderHook(() => + useRecommendationShelf({ + campaignVrn: CROSS_SELL_VRN, + enableRecommendations: true, + itemsContext: 'CART', + }) + ) + + expect(lastArgs()).toBeNull() + }) + + it('still fetches context-agnostic campaigns on an empty cart', () => { + renderHook(() => + useRecommendationShelf({ + campaignVrn: TOP_ITEMS_VRN, + enableRecommendations: true, + itemsContext: 'CART', + }) + ) + + expect(lastArgs()).toEqual({ + userId: 'user-1', + campaignVrn: TOP_ITEMS_VRN, + products: [], + }) + }) + + it('follows the live cart context when not frozen', () => { + useCart.mockReturnValue({ items: [cartItem('pg-1')] }) + + const { rerender } = renderHook(() => + useRecommendationShelf({ + campaignVrn: CROSS_SELL_VRN, + enableRecommendations: true, + itemsContext: 'CART', + }) + ) + + useCart.mockReturnValue({ items: [cartItem('pg-1'), cartItem('pg-2')] }) + rerender() + + expect(lastArgs()?.products).toEqual(['pg-1', 'pg-2']) + }) + + it('anchors PDP-context campaigns on the current product', () => { + usePDP.mockReturnValue({ + data: { product: { isVariantOf: { productGroupID: 'pg-pdp' } } }, + }) + + renderHook(() => + useRecommendationShelf({ + campaignVrn: CROSS_SELL_VRN, + enableRecommendations: true, + }) + ) + + expect(lastArgs()?.products).toEqual(['pg-pdp']) + }) + + it('returns no items and logs without the userId on error', () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + useRecommendations.mockReturnValue({ + data: undefined, + isLoading: false, + error: new Error('boom'), + }) + + const { result } = renderHook(() => + useRecommendationShelf({ + campaignVrn: TOP_ITEMS_VRN, + enableRecommendations: true, + }) + ) + + expect(result.current.items).toEqual([]) + expect(result.current.error).toBeInstanceOf(Error) + expect(consoleError).toHaveBeenCalled() + expect(JSON.stringify(consoleError.mock.calls)).not.toContain('user-1') + }) +}) diff --git a/packages/core/test/components/RecommendationShelf/useRecommendationUserId.test.ts b/packages/core/test/sdk/recommendations/useRecommendationUserId.test.ts similarity index 94% rename from packages/core/test/components/RecommendationShelf/useRecommendationUserId.test.ts rename to packages/core/test/sdk/recommendations/useRecommendationUserId.test.ts index 7f22144b4e..0624385da3 100644 --- a/packages/core/test/components/RecommendationShelf/useRecommendationUserId.test.ts +++ b/packages/core/test/sdk/recommendations/useRecommendationUserId.test.ts @@ -14,7 +14,7 @@ vi.mock('src/utils/retry', () => ({ retry: (fn: () => unknown) => Promise.resolve().then(fn), })) -import { useRecommendationUserId } from 'src/components/sections/RecommendationShelf/useRecommendationUserId' +import { useRecommendationUserId } from 'src/sdk/recommendations/useRecommendationUserId' const CAMPAIGN_VRN = 'vrn:recommendations:acc:rec-top-items-v2:campaign-1' diff --git a/packages/core/test/components/RecommendationShelf/useRecommendations.test.ts b/packages/core/test/sdk/recommendations/useRecommendations.test.ts similarity index 93% rename from packages/core/test/components/RecommendationShelf/useRecommendations.test.ts rename to packages/core/test/sdk/recommendations/useRecommendations.test.ts index 68db7d1cc4..daeb69c341 100644 --- a/packages/core/test/components/RecommendationShelf/useRecommendations.test.ts +++ b/packages/core/test/sdk/recommendations/useRecommendations.test.ts @@ -7,7 +7,7 @@ vi.mock('src/sdk/graphql/useQuery', () => ({ useQuery: useQueryMock, })) -import { useRecommendations } from 'src/components/sections/RecommendationShelf/useRecommendations' +import { useRecommendations } from 'src/sdk/recommendations/useRecommendations' afterEach(() => { vi.clearAllMocks() diff --git a/packages/core/test/components/RecommendationShelf/vrn.test.ts b/packages/core/test/sdk/recommendations/vrn.test.ts similarity index 94% rename from packages/core/test/components/RecommendationShelf/vrn.test.ts rename to packages/core/test/sdk/recommendations/vrn.test.ts index affebdd096..9a7075d316 100644 --- a/packages/core/test/components/RecommendationShelf/vrn.test.ts +++ b/packages/core/test/sdk/recommendations/vrn.test.ts @@ -1,9 +1,6 @@ import { describe, expect, it } from 'vitest' -import { - getTypeFromVrn, - isValidVrn, -} from 'src/components/sections/RecommendationShelf/vrn' +import { getTypeFromVrn, isValidVrn } from 'src/sdk/recommendations/vrn' const vrn = (type: string) => `vrn:recommendations:account:${type}:campaign-1` diff --git a/packages/core/test/sdk/session/syncSalesChannelFromOrderForm.test.ts b/packages/core/test/sdk/session/syncSalesChannelFromOrderForm.test.ts new file mode 100644 index 0000000000..7b325f4116 --- /dev/null +++ b/packages/core/test/sdk/session/syncSalesChannelFromOrderForm.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, it, vi } from 'vitest' +import type { Session } from '@faststore/sdk' +import { syncSalesChannelFromOrderForm } from '../../../src/sdk/session/syncSalesChannelFromOrderForm' + +const baseSession = ( + salesChannel: string, + explicit = true, + channelOverride?: string | null +): Session => ({ + currency: { code: 'BRL', symbol: 'R$' }, + locale: 'pt-BR', + country: 'BRA', + channel: + channelOverride !== undefined + ? channelOverride + : JSON.stringify({ + salesChannel, + regionId: '', + hasOnlyDefaultSalesChannel: !explicit, + }), + deliveryMode: null, + addressType: null, + city: null, + postalCode: null, + geoCoordinates: null, + person: null, + b2b: null, + marketingData: null, + refreshAfter: null, +}) + +describe('syncSalesChannelFromOrderForm', () => { + it('updates session channel silently when SC diverges', () => { + const setSilent = vi.fn() + const synced = syncSalesChannelFromOrderForm( + '2', + () => baseSession('1', false), + setSilent + ) + + expect(synced).toBe(true) + expect(setSilent).toHaveBeenCalledTimes(1) + const next = setSilent.mock.calls[0][0] as Session + expect(JSON.parse(next.channel ?? '{}')).toMatchObject({ + salesChannel: '2', + hasOnlyDefaultSalesChannel: false, + }) + }) + + it('is a no-op when SC already matches', () => { + const setSilent = vi.fn() + const synced = syncSalesChannelFromOrderForm( + '2', + () => baseSession('2'), + setSilent + ) + + expect(synced).toBe(false) + expect(setSilent).not.toHaveBeenCalled() + }) + + it('is a no-op when adopted SC is missing', () => { + const setSilent = vi.fn() + expect( + syncSalesChannelFromOrderForm(null, () => baseSession('1'), setSilent) + ).toBe(false) + expect( + syncSalesChannelFromOrderForm('', () => baseSession('1'), setSilent) + ).toBe(false) + expect(setSilent).not.toHaveBeenCalled() + }) + + it('resets non-object channel JSON before adopting SC', () => { + const setSilent = vi.fn() + + for (const invalid of ['null', '[]', '"text"', '0', 'false']) { + setSilent.mockClear() + const synced = syncSalesChannelFromOrderForm( + '2', + () => baseSession('1', true, invalid), + setSilent + ) + + expect(synced).toBe(true) + expect(JSON.parse(setSilent.mock.calls[0][0].channel)).toMatchObject({ + salesChannel: '2', + hasOnlyDefaultSalesChannel: false, + }) + } + }) + + it('resets malformed channel JSON before adopting SC', () => { + const setSilent = vi.fn() + const synced = syncSalesChannelFromOrderForm( + '2', + () => baseSession('1', true, '{'), + setSilent + ) + + expect(synced).toBe(true) + expect(JSON.parse(setSilent.mock.calls[0][0].channel)).toMatchObject({ + salesChannel: '2', + hasOnlyDefaultSalesChannel: false, + }) + }) + + it('ignores object-valued salesChannel when comparing', () => { + const setSilent = vi.fn() + const synced = syncSalesChannelFromOrderForm( + '2', + () => + baseSession( + '1', + true, + JSON.stringify({ salesChannel: { nested: true } }) + ), + setSilent + ) + + expect(synced).toBe(true) + expect(JSON.parse(setSilent.mock.calls[0][0].channel).salesChannel).toBe( + '2' + ) + }) +}) diff --git a/packages/core/test/server/rootSpan.test.ts b/packages/core/test/server/rootSpan.test.ts new file mode 100644 index 0000000000..a6bea07f4b --- /dev/null +++ b/packages/core/test/server/rootSpan.test.ts @@ -0,0 +1,90 @@ +import { OTELAPI } from '@faststore/diagnostics' +import { + afterAll, + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest' + +import { execute } from '../../src/server' +import { apiOptions } from '../../src/server/options' + +const span = { + end: vi.fn(), + setStatus: vi.fn(), + recordException: vi.fn(), +} + +const startSpan = vi.fn(() => span) + +const otelEnabledByDefault = apiOptions.OTEL_ENABLED + +// `__typename` resolves inside GraphQL itself, so these run the whole envelop +// pipeline without reaching the VTEX platform. +const runOperation = (query: string, operationName?: string) => + execute({ + operation: { __meta__: { operationName, operationHash: 'not-persisted' } }, + variables: {}, + query, + }) + +beforeEach(() => { + vi.clearAllMocks() + vi.spyOn(OTELAPI.trace, 'getTracer').mockReturnValue({ startSpan } as never) + apiOptions.OTEL_ENABLED = true +}) + +afterEach(() => { + vi.restoreAllMocks() +}) + +afterAll(() => { + apiOptions.OTEL_ENABLED = otelEnabledByDefault +}) + +describe('execute root span', () => { + it('wraps the operation in a span named after it', async () => { + const { data } = await runOperation( + 'query RootSpanQuery { __typename }', + 'RootSpanQuery' + ) + + expect(data).toEqual({ __typename: 'Query' }) + expect(startSpan).toHaveBeenCalledWith('graphql RootSpanQuery') + expect(span.end).toHaveBeenCalledTimes(1) + expect(span.setStatus).not.toHaveBeenCalled() + }) + + it('names the span generically when the operation is anonymous', async () => { + await runOperation('{ __typename }') + + expect(startSpan).toHaveBeenCalledWith('graphql operation') + }) + + it('marks the span as failed and ends it when the operation throws', async () => { + await expect( + runOperation('this is not graphql', 'BrokenQuery') + ).rejects.toThrow(/Syntax Error/) + + expect(span.setStatus).toHaveBeenCalledWith({ + code: OTELAPI.SpanStatusCode.ERROR, + }) + expect(span.recordException).toHaveBeenCalledWith(expect.any(Error)) + expect(span.end).toHaveBeenCalledTimes(1) + }) + + it('skips instrumentation entirely when telemetry is disabled', async () => { + apiOptions.OTEL_ENABLED = false + + const { data } = await runOperation( + 'query RootSpanQuery { __typename }', + 'RootSpanQuery' + ) + + expect(data).toEqual({ __typename: 'Query' }) + expect(startSpan).not.toHaveBeenCalled() + }) +}) diff --git a/packages/diagnostics/@types/global.d.ts b/packages/diagnostics/@types/global.d.ts index e8a5236b47..d459071e59 100644 --- a/packages/diagnostics/@types/global.d.ts +++ b/packages/diagnostics/@types/global.d.ts @@ -1,12 +1,11 @@ -/// - -import type { TelemetryClient } from '@vtex/diagnostics-nodejs/dist/telemetry/client.js' -import type { TraceClient } from '@vtex/diagnostics-nodejs/dist/types/traces.js' +import type { NodeSDK } from '@opentelemetry/sdk-node' declare global { var fsDiagnostics: { - TELEMETRY_CLIENTS: Map - TRACE_CLIENTS: Map + TELEMETRY_CLIENT: NodeSDK | undefined IS_DEV: boolean + OTLP_TRACES_ENDPOINT: string + OTLP_LOGGER_ENDPOINT: string + OTLP_TRACES_SAMPLE_RATE: number } } diff --git a/packages/diagnostics/CHANGELOG.md b/packages/diagnostics/CHANGELOG.md index 2741e1b8fc..0c341fdc8f 100644 --- a/packages/diagnostics/CHANGELOG.md +++ b/packages/diagnostics/CHANGELOG.md @@ -3,6 +3,44 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.6.0-dev.6](https://github.com/vtex/faststore/compare/v4.6.0-dev.5...v4.6.0-dev.6) (2026-08-14) + +**Note:** Version bump only for package @faststore/diagnostics + +# [4.6.0-dev.5](https://github.com/vtex/faststore/compare/v4.6.0-dev.4...v4.6.0-dev.5) (2026-08-13) + +**Note:** Version bump only for package @faststore/diagnostics + +# [4.6.0-dev.4](https://github.com/vtex/faststore/compare/v4.6.0-dev.3...v4.6.0-dev.4) (2026-08-12) + +**Note:** Version bump only for package @faststore/diagnostics + +# [4.6.0-dev.3](https://github.com/vtex/faststore/compare/v4.6.0-dev.2...v4.6.0-dev.3) (2026-08-12) + +**Note:** Version bump only for package @faststore/diagnostics + +# [4.6.0-dev.2](https://github.com/vtex/faststore/compare/v4.6.0-dev.1...v4.6.0-dev.2) (2026-08-12) + +**Note:** Version bump only for package @faststore/diagnostics + +# [4.6.0-dev.1](https://github.com/vtex/faststore/compare/v4.6.0-dev.0...v4.6.0-dev.1) (2026-08-11) + +## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.5.1) (2026-08-11) + +**Note:** Version bump only for package @faststore/diagnostics + +# [4.6.0-dev.0](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.6.0-dev.0) (2026-08-11) + +### Features + +- removing vtex/diagnostics-nodejs ([#3389](https://github.com/vtex/faststore/issues/3389)) ([74e2d50](https://github.com/vtex/faststore/commit/74e2d503a44903929b90af7701956536690d4e41)) + +## [4.5.1-dev.4](https://github.com/vtex/faststore/compare/v4.5.1-dev.3...v4.5.1-dev.4) (2026-08-10) + +**Note:** Version bump only for package @faststore/diagnostics + +## [4.5.1-dev.3](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1-dev.3) (2026-08-10) + ## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1) (2026-08-11) **Note:** Version bump only for package @faststore/diagnostics diff --git a/packages/diagnostics/README.md b/packages/diagnostics/README.md index ae721b6c3e..810b0efd20 100644 --- a/packages/diagnostics/README.md +++ b/packages/diagnostics/README.md @@ -17,18 +17,18 @@

-`@faststore/diagnostics` initializes OpenTelemetry tracing, logging, and metrics for FastStore server-side instrumentation. It is a thin wrapper around `@vtex/diagnostics-nodejs` and is consumed by `@faststore/core` via Next.js instrumentation when telemetry is enabled. +`@faststore/diagnostics` initializes OpenTelemetry tracing and logging for FastStore server-side instrumentation. ## Package structure ```text src/ -├── globals.ts # Initializes the global fsDiagnostics state (telemetry client map, IS_DEV flag) -├── start.ts # getTelemetryClient() and getTraceClient() implementations -└── index.ts # Public exports -configs/ -├── dev.json # Telemetry client config for development -└── prod.json # Telemetry client config for production +├── globals.ts # Initializes the global fsDiagnostics state (endpoints, sample rate, IS_DEV flag) +├── credentials.ts # gRPC channel credentials shared by both exporters +├── tracer.ts # Span processor and sampler +├── logger.ts # Log record processor and the logger() emitter +├── start.ts # getTelemetryClient() implementation +└── index.ts # Public exports ``` ## How it works @@ -48,26 +48,43 @@ await getTelemetryClient({ }) ``` -This initializes traces, logs, and metrics clients and registers HTTP instrumentation. Telemetry data is exported to an OTLP endpoint. +This starts a single `NodeSDK` per process, which registers the global tracer and logger providers and installs HTTP and Undici instrumentation. Telemetry is exported to an OTLP gRPC endpoint. + +Application code emits logs through `logger()`, which resolves the provider on each call and is a no-op until the SDK has started: + +```ts +import { logger } from '@faststore/diagnostics' + +const log = logger('@faststore/core') + +// printf-style: arguments are only formatted when telemetry is enabled +log('error', 'Request failed: %s', reason) +``` ## Configuration | Variable | Default | Description | | :--- | :--- | :--- | -| `OTLP_TRACES_ENDPOINT` | `localhost:4317` | OTLP gRPC endpoint for trace export | -| `NODE_ENV` | — | `production` disables dev mode and uses `configs/prod.json` | +| `OTLP_TRACES_ENDPOINT` | VTEX traces collector | OTLP gRPC endpoint for trace export | +| `OTLP_LOGGER_ENDPOINT` | VTEX logs collector | OTLP gRPC endpoint for log export | +| `OTLP_TRACES_SAMPLE_RATE` | `0.3` | Fraction of traces exported outside development (development always samples everything) | +| `OTEL_EXPORTER_OTLP_COMPRESSION` | — | Set to `gzip` to compress exported payloads | +| `NODE_ENV` | — | Any value other than `production` enables dev mode | + +Endpoints connect over plaintext gRPC unless the configured URL starts with `https://`. -To enable telemetry in a store, set `analytics.otelEnabled: true` in `discovery.config.js`. +Telemetry is off until the feature is released: `@faststore/core` gates SDK startup behind `instrumentationEnabled` in `src/instrumentation.ts` and resolver spans behind `OTEL_ENABLED` in `src/server/options.ts`. See the [observability runbook](../../docs/observability.md#enabling). ## How to develop -All logic lives in `src/start.ts` and `src/globals.ts`. To make changes: +To make changes: 1. Edit the relevant file in `src/` 2. Run `pnpm build` to compile -3. Verify via `@faststore/core` — since it's a workspace dependency, `instrumentation.ts` picks up your local build automatically +3. Run `pnpm test` for the unit tests +4. Verify via `@faststore/core` — since it's a workspace dependency, `instrumentation.ts` picks up your local build automatically -> Changes are validated by running `@faststore/core` with `analytics.otelEnabled: true` and checking that traces reach the configured OTLP endpoint. +> Changes are validated by running `@faststore/core` with both gating flags set to `true` and checking that traces and logs reach the configured OTLP endpoints. ## How to run @@ -92,7 +109,5 @@ Versioning and publishing are managed at the monorepo root by Lerna. Do not publ ## Documentation -- **Sampling config (dev):** [`configs/dev.json`](./configs/dev.json) — 100% sample rate -- **Sampling config (prod):** [`configs/prod.json`](./configs/prod.json) — 1% default, 30% for `trace_all` -- **Upstream library:** [@vtex/diagnostics-nodejs](https://www.npmjs.com/package/@vtex/diagnostics-nodejs) +- **Observability runbook:** [`docs/observability.md`](../../docs/observability.md) - **OpenTelemetry:** [opentelemetry.io](https://opentelemetry.io) diff --git a/packages/diagnostics/configs/dev.json b/packages/diagnostics/configs/dev.json deleted file mode 100644 index 96a2f29ca9..0000000000 --- a/packages/diagnostics/configs/dev.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "traces": { - "sampling": { - "defaultRate": 1, - "rules": [ - { - "name": "debug_all", - "sampleRate": 1, - "conditions": [] - } - ] - } - } -} diff --git a/packages/diagnostics/configs/prod.json b/packages/diagnostics/configs/prod.json deleted file mode 100644 index 0be3b773aa..0000000000 --- a/packages/diagnostics/configs/prod.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "traces": { - "sampling": { - "defaultRate": 0.01, - "rules": [ - { - "name": "trace_all", - "sampleRate": 0.3, - "conditions": [] - } - ] - } - } -} diff --git a/packages/diagnostics/package.json b/packages/diagnostics/package.json index 1d0f4643a8..8e6c4c8abd 100644 --- a/packages/diagnostics/package.json +++ b/packages/diagnostics/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/diagnostics", - "version": "4.5.1", + "version": "4.6.0-dev.6", "description": "", "repository": { "type": "git", @@ -8,39 +8,46 @@ "directory": "packages/diagnostics" }, "main": "./dist/cjs/index.js", - "types": "./dist/src/index.d.ts", + "types": "./dist/index.d.ts", "scripts": { - "build": "vite build" + "build": "vite build", + "test": "vitest run" }, "keywords": [], "author": "", "license": "MIT", "files": [ - "configs", "dist" ], "exports": { ".": { - "types": "./dist/src/index.d.ts", + "types": "./dist/index.d.ts", "require": "./dist/cjs/index.js", "import": "./dist/es/index.mjs", - "default": "./dist/cjs/index.js" - }, - "./configs/*": "./configs/*" + "default": "./dist/es/index.mjs" + } }, "packageManager": "pnpm@10.28.0", "dependencies": { + "@grpc/grpc-js": "^1.14.4", "@opentelemetry/api": "^1.9.0", - "@opentelemetry/instrumentation-http": "^0.212.0", - "@vtex/diagnostics-nodejs": "^5.1.1", - "resolve-pkg": "^3.0.1" + "@opentelemetry/api-logs": "^0.219.0", + "@opentelemetry/exporter-logs-otlp-grpc": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.219.0", + "@opentelemetry/instrumentation-http": "^0.219.0", + "@opentelemetry/instrumentation-undici": "^0.29.0", + "@opentelemetry/resources": "^2.8.0", + "@opentelemetry/sdk-logs": "^0.219.0", + "@opentelemetry/sdk-node": "^0.219.0", + "@opentelemetry/sdk-trace-node": "^2.8.0" }, "devDependencies": { "@types/node": "catalog:", "tslib": "catalog:", "typescript": "catalog:", "vite": "catalog:", - "vite-plugin-dts": "catalog:" + "vite-plugin-dts": "catalog:", + "vitest": "catalog:" }, "publishConfig": { "access": "public" diff --git a/packages/diagnostics/src/credentials.ts b/packages/diagnostics/src/credentials.ts new file mode 100644 index 0000000000..9797f986d8 --- /dev/null +++ b/packages/diagnostics/src/credentials.ts @@ -0,0 +1,12 @@ +import { type ChannelCredentials, credentials } from '@grpc/grpc-js' + +/** + * The VTEX collectors are reached over plaintext gRPC (port 80), so transport + * security is opt-in via an explicit `https://` endpoint rather than inferred + * from the environment. + */ +export function createChannelCredentials(endpoint: string): ChannelCredentials { + return endpoint.startsWith('https://') + ? credentials.createSsl() + : credentials.createInsecure() +} diff --git a/packages/diagnostics/src/globals.ts b/packages/diagnostics/src/globals.ts index a0236ecd98..cbdf5d5f77 100644 --- a/packages/diagnostics/src/globals.ts +++ b/packages/diagnostics/src/globals.ts @@ -1,5 +1,32 @@ +const DEFAULT_TRACES_ENDPOINT = + 'traces-grpc-faststore-pvl.opentelemetry-collector.vtex.systems:80' +const DEFAULT_LOGGER_ENDPOINT = + 'logs-developer-fluentd-faststore-pvl.opentelemetry-collector.vtex.systems' + +/** Fraction of traces exported when telemetry is enabled outside development. */ +const DEFAULT_TRACES_SAMPLE_RATE = 0.3 + +const parseSampleRate = (value: string | undefined, fallback: number) => { + if (!value) { + return fallback + } + + const parsed = Number(value) + + return Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 + ? parsed + : fallback +} + globalThis.fsDiagnostics ??= { - IS_DEV: import.meta.env.DEV === true || process.env.NODE_ENV !== 'production', - TELEMETRY_CLIENTS: new Map(), - TRACE_CLIENTS: new Map(), + IS_DEV: process.env.NODE_ENV !== 'production', + TELEMETRY_CLIENT: undefined, + OTLP_TRACES_ENDPOINT: + process.env.OTLP_TRACES_ENDPOINT || DEFAULT_TRACES_ENDPOINT, + OTLP_LOGGER_ENDPOINT: + process.env.OTLP_LOGGER_ENDPOINT || DEFAULT_LOGGER_ENDPOINT, + OTLP_TRACES_SAMPLE_RATE: parseSampleRate( + process.env.OTLP_TRACES_SAMPLE_RATE, + DEFAULT_TRACES_SAMPLE_RATE + ), } diff --git a/packages/diagnostics/src/index.ts b/packages/diagnostics/src/index.ts index e9956b6ff0..6d33f945d5 100644 --- a/packages/diagnostics/src/index.ts +++ b/packages/diagnostics/src/index.ts @@ -1,3 +1,4 @@ import './globals' export * as OTELAPI from '@opentelemetry/api' -export { getTelemetryClient, getTraceClient } from './start' +export { type LogSeverity, logger } from './logger' +export { getTelemetryClient, type TelemetryOptions } from './start' diff --git a/packages/diagnostics/src/logger.ts b/packages/diagnostics/src/logger.ts new file mode 100644 index 0000000000..532b982a84 --- /dev/null +++ b/packages/diagnostics/src/logger.ts @@ -0,0 +1,48 @@ +import { SeverityNumber, logs } from '@opentelemetry/api-logs' +import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc' +import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs' +import { format } from 'node:util' +import { createChannelCredentials } from './credentials' + +export function getLoggerExporter() { + const endpoint = globalThis.fsDiagnostics.OTLP_LOGGER_ENDPOINT + + return new BatchLogRecordProcessor( + new OTLPLogExporter({ + credentials: createChannelCredentials(endpoint), + url: endpoint, + }) + ) +} + +const SEVERITY = { + error: { number: SeverityNumber.ERROR, text: 'ERROR' }, + warn: { number: SeverityNumber.WARN, text: 'WARN' }, + info: { number: SeverityNumber.INFO, text: 'INFO' }, + debug: { number: SeverityNumber.DEBUG, text: 'DEBUG' }, +} as const + +export type LogSeverity = keyof typeof SEVERITY + +/** + * Builds a `util.format`-style emitter for the given instrumentation scope. + * + * The underlying provider is resolved on every call because callers hold these + * emitters in module-level constants, which are evaluated before + * `getTelemetryClient` installs the provider. Formatting is deferred until a + * provider exists so disabled telemetry costs nothing on hot paths — pass + * printf-style arguments rather than pre-built template strings. + */ +export function logger(name: string) { + return (severity: LogSeverity, ...args: unknown[]) => { + if (!globalThis.fsDiagnostics.TELEMETRY_CLIENT) { + return + } + + logs.getLogger(name).emit({ + severityNumber: SEVERITY[severity].number, + severityText: SEVERITY[severity].text, + body: format(...args), + }) + } +} diff --git a/packages/diagnostics/src/start.ts b/packages/diagnostics/src/start.ts index 125f5345aa..31ad4dd4b8 100644 --- a/packages/diagnostics/src/start.ts +++ b/packages/diagnostics/src/start.ts @@ -1,83 +1,73 @@ -import { - CreateTracesExporterConfig, - CreateExporter, -} from '@vtex/diagnostics-nodejs/dist/exporters/index.js' -import { NewTelemetryClient } from '@vtex/diagnostics-nodejs/dist/telemetry/client.js' import { HttpInstrumentation } from '@opentelemetry/instrumentation-http' -import resolvePackage from 'resolve-pkg' +import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici' +import { resourceFromAttributes } from '@opentelemetry/resources' +import { NodeSDK } from '@opentelemetry/sdk-node' +import { getLoggerExporter } from './logger' +import { traceExporter, traceSampler } from './tracer' -import { name as currentPkgName } from '../package.json' with { type: 'json' } - -async function setupTracesExporter() { - const OTLP_TRACES_ENDPOINT = - process.env.OTLP_TRACES_ENDPOINT || 'localhost:4317' - - const tracesConfig = CreateTracesExporterConfig({ - endpoint: OTLP_TRACES_ENDPOINT, - insecure: global.fsDiagnostics.IS_DEV ?? false, - }) - - const tracesExporter = CreateExporter(tracesConfig, 'otlp') - await tracesExporter.initialize() - return tracesExporter -} - -export async function getTelemetryClient(opt: { +export interface TelemetryOptions { serviceName: string version: string clientName: string account: string packageName: string -}) { - if (global.fsDiagnostics.TELEMETRY_CLIENTS.has(opt.packageName)) - return global.fsDiagnostics.TELEMETRY_CLIENTS.get(opt.packageName) +} - const client = await NewTelemetryClient( - currentPkgName, - opt.clientName, - opt.serviceName, - { - additionalAttrs: { - '@faststore_version': opt.version, - '@faststore_package_name': opt.packageName, - '@faststore_account_name': opt.account ?? 'unknown', - '@faststore_environment': process.env.NODE_ENV ?? 'development', - }, - // debug: global.fsDiagnostics.IS_DEV, - config: { - configPath: resolvePackage( - `@faststore/diagnostics/configs/${global.fsDiagnostics.IS_DEV ? 'dev' : 'prod'}.json`, - { - cwd: process.env.PWD ?? process.cwd(), - } - ), - }, - } - ) +/** + * Boots the OpenTelemetry SDK once per process and registers it globally, so + * `logger()` and `OTELAPI.trace` pick it up. Repeat calls return the running + * instance, which also keeps Next.js hot-module-reload from starting a second + * exporter in development. + */ +export async function getTelemetryClient(opt: TelemetryOptions) { + if (globalThis.fsDiagnostics.TELEMETRY_CLIENT) { + return globalThis.fsDiagnostics.TELEMETRY_CLIENT + } - const tracesExporter = await setupTracesExporter() - const logger = await client.newLogsClient() - const metrics = await client.newMetricsClient() + const resource = resourceFromAttributes({ + 'service.name': opt.serviceName, + 'service.version': opt.version, + 'vtex.account.name': opt.account ?? 'unknown', + 'vtex.application.id': opt.serviceName, + 'vtex.diagnostics.name': 'faststore_custom', + '@faststore_version': opt.version, + '@faststore_package_name': opt.packageName, + '@faststore_account_name': opt.account ?? 'unknown', + '@faststore_environment': process.env.NODE_ENV ?? 'development', + }) - global.fsDiagnostics.TRACE_CLIENTS.set( - opt.packageName, - await client.newTracesClient({ - setGlobalProvider: true, - exporter: tracesExporter, - }) - ) + // NodeSDK owns both pipelines: it installs the global tracer provider and, + // via `logRecordProcessors`, the global logger provider that `logger()` reads. + const sdk = new NodeSDK({ + resource, + sampler: traceSampler(), + spanProcessors: [traceExporter()], + logRecordProcessors: [getLoggerExporter()], + instrumentations: [new HttpInstrumentation(), new UndiciInstrumentation()], + }) - client.registerInstrumentations([new HttpInstrumentation()]) + sdk.start() + globalThis.fsDiagnostics.TELEMETRY_CLIENT = sdk - await logger.shutdown() - await metrics.shutdown() + registerShutdownHook(sdk) - if (global.fsDiagnostics.IS_DEV) console.log('TELEMETRY CLIENT STARTED', opt) + if (globalThis.fsDiagnostics.IS_DEV) { + console.log('TELEMETRY CLIENT STARTED', opt) + } - global.fsDiagnostics.TELEMETRY_CLIENTS.set(opt.packageName, client) - return client + return sdk } -export function getTraceClient(serviceName: string) { - return global.fsDiagnostics.TRACE_CLIENTS.get(serviceName) +/** + * Flushes pending spans and logs on SIGTERM. Registering a listener suppresses + * Node's default termination, so the signal is re-raised once the listener has + * been removed instead of forcing `process.exit` on the host application. + */ +function registerShutdownHook(sdk: NodeSDK) { + process.once('SIGTERM', () => { + sdk + .shutdown() + .catch((error) => console.error('Error shutting down OTel SDK', error)) + .finally(() => process.kill(process.pid, 'SIGTERM')) + }) } diff --git a/packages/diagnostics/src/tracer.ts b/packages/diagnostics/src/tracer.ts new file mode 100644 index 0000000000..3b0b508cb5 --- /dev/null +++ b/packages/diagnostics/src/tracer.ts @@ -0,0 +1,41 @@ +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc' +import { + BatchSpanProcessor, + ParentBasedSampler, + type Sampler, + TraceIdRatioBasedSampler, +} from '@opentelemetry/sdk-trace-node' +import { createChannelCredentials } from './credentials' + +/** + * Payload compression follows the standard `OTEL_EXPORTER_OTLP_COMPRESSION` + * environment variable (set it to `gzip` to reduce egress). + */ +export function traceExporter() { + const endpoint = globalThis.fsDiagnostics.OTLP_TRACES_ENDPOINT + + const collectorExporter = new OTLPTraceExporter({ + credentials: createChannelCredentials(endpoint), + url: endpoint, + }) + + return new BatchSpanProcessor(collectorExporter, { + maxQueueSize: 2048, + maxExportBatchSize: 512, + scheduledDelayMillis: 5000, + exportTimeoutMillis: 30_000, + }) +} + +/** + * Development traces everything; elsewhere a ratio sampler keeps export volume + * (and collector cost) bounded. Parent-based so a sampled request keeps all of + * its child spans instead of producing partial traces. + */ +export function traceSampler(): Sampler { + const ratio = globalThis.fsDiagnostics.IS_DEV + ? 1 + : globalThis.fsDiagnostics.OTLP_TRACES_SAMPLE_RATE + + return new ParentBasedSampler({ root: new TraceIdRatioBasedSampler(ratio) }) +} diff --git a/packages/diagnostics/test/logger.test.ts b/packages/diagnostics/test/logger.test.ts new file mode 100644 index 0000000000..b2e938d9d0 --- /dev/null +++ b/packages/diagnostics/test/logger.test.ts @@ -0,0 +1,81 @@ +import { logs } from '@opentelemetry/api-logs' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import '../src/globals' +import { logger } from '../src/logger' + +const emit = vi.fn() + +beforeEach(() => { + emit.mockClear() + globalThis.fsDiagnostics.TELEMETRY_CLIENT = undefined + vi.spyOn(logs, 'getLogger').mockReturnValue({ emit } as never) +}) + +afterEach(() => { + vi.restoreAllMocks() + globalThis.fsDiagnostics.TELEMETRY_CLIENT = undefined +}) + +/** Stands in for a started NodeSDK. */ +const startTelemetry = () => { + globalThis.fsDiagnostics.TELEMETRY_CLIENT = {} as never +} + +describe('logger', () => { + it('does not emit while telemetry is disabled', () => { + logger('@faststore/core')('info', 'hello') + + expect(emit).not.toHaveBeenCalled() + }) + + it('does not format arguments while telemetry is disabled', () => { + const expensive = { toString: vi.fn(() => 'expensive') } + + logger('@faststore/core')('debug', '%s', expensive) + + expect(expensive.toString).not.toHaveBeenCalled() + }) + + // Regression: emitters are held in module-level constants that are evaluated + // before the SDK starts, so the provider must be resolved per call. + it('emits once telemetry starts after the emitter was created', () => { + const log = logger('@faststore/core') + + log('info', 'before') + expect(emit).not.toHaveBeenCalled() + + startTelemetry() + log('info', 'after') + + expect(emit).toHaveBeenCalledTimes(1) + expect(emit.mock.calls[0][0]).toMatchObject({ + severityText: 'INFO', + body: 'after', + }) + }) + + it('applies printf-style formatting rather than inspecting the argument list', () => { + startTelemetry() + + logger('@faststore/api')('error', 'Error at resolver: %s', 'StoreProduct') + + expect(emit.mock.calls[0][0].body).toBe('Error at resolver: StoreProduct') + }) + + it('maps each severity to its OTel severity number', () => { + startTelemetry() + const log = logger('@faststore/core') + + for (const severity of ['error', 'warn', 'info', 'debug'] as const) { + log(severity, 'message') + } + + expect(emit.mock.calls.map(([record]) => record.severityText)).toEqual([ + 'ERROR', + 'WARN', + 'INFO', + 'DEBUG', + ]) + }) +}) diff --git a/packages/diagnostics/vite.config.mts b/packages/diagnostics/vite.config.mts index 6747d23ccf..eb6e3a78e2 100644 --- a/packages/diagnostics/vite.config.mts +++ b/packages/diagnostics/vite.config.mts @@ -18,7 +18,6 @@ export default defineConfig(({ mode }) => ({ }, rollupOptions: { external: [ - /@vtex\/diagnostics-nodejs/, /@opentelemetry/, ...builtinModules.concat(builtinModules.map((e) => `node:${e}`)), ...Object.keys({ diff --git a/packages/lighthouse/CHANGELOG.md b/packages/lighthouse/CHANGELOG.md index 2ce57599cc..8bf0e07cc4 100644 --- a/packages/lighthouse/CHANGELOG.md +++ b/packages/lighthouse/CHANGELOG.md @@ -3,6 +3,42 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.6.0-dev.6](https://github.com/vtex/faststore/compare/v4.6.0-dev.5...v4.6.0-dev.6) (2026-08-14) + +**Note:** Version bump only for package @faststore/lighthouse + +# [4.6.0-dev.5](https://github.com/vtex/faststore/compare/v4.6.0-dev.4...v4.6.0-dev.5) (2026-08-13) + +**Note:** Version bump only for package @faststore/lighthouse + +# [4.6.0-dev.4](https://github.com/vtex/faststore/compare/v4.6.0-dev.3...v4.6.0-dev.4) (2026-08-12) + +**Note:** Version bump only for package @faststore/lighthouse + +# [4.6.0-dev.3](https://github.com/vtex/faststore/compare/v4.6.0-dev.2...v4.6.0-dev.3) (2026-08-12) + +**Note:** Version bump only for package @faststore/lighthouse + +# [4.6.0-dev.2](https://github.com/vtex/faststore/compare/v4.6.0-dev.1...v4.6.0-dev.2) (2026-08-12) + +**Note:** Version bump only for package @faststore/lighthouse + +# [4.6.0-dev.1](https://github.com/vtex/faststore/compare/v4.6.0-dev.0...v4.6.0-dev.1) (2026-08-11) + +## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.5.1) (2026-08-11) + +**Note:** Version bump only for package @faststore/lighthouse + +# [4.6.0-dev.0](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.6.0-dev.0) (2026-08-11) + +**Note:** Version bump only for package @faststore/lighthouse + +## [4.5.1-dev.4](https://github.com/vtex/faststore/compare/v4.5.1-dev.3...v4.5.1-dev.4) (2026-08-10) + +**Note:** Version bump only for package @faststore/lighthouse + +## [4.5.1-dev.3](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1-dev.3) (2026-08-10) + ## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1) (2026-08-11) **Note:** Version bump only for package @faststore/lighthouse diff --git a/packages/lighthouse/package.json b/packages/lighthouse/package.json index daebaf3c2e..e27bdf5e23 100644 --- a/packages/lighthouse/package.json +++ b/packages/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/lighthouse", - "version": "4.5.1", + "version": "4.6.0-dev.6", "author": "Emerson Laurentino", "license": "MIT", "repository": { diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index d4238f83aa..45766439ae 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -3,6 +3,42 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.6.0-dev.6](https://github.com/vtex/faststore/compare/v4.6.0-dev.5...v4.6.0-dev.6) (2026-08-14) + +**Note:** Version bump only for package @faststore/sdk + +# [4.6.0-dev.5](https://github.com/vtex/faststore/compare/v4.6.0-dev.4...v4.6.0-dev.5) (2026-08-13) + +**Note:** Version bump only for package @faststore/sdk + +# [4.6.0-dev.4](https://github.com/vtex/faststore/compare/v4.6.0-dev.3...v4.6.0-dev.4) (2026-08-12) + +**Note:** Version bump only for package @faststore/sdk + +# [4.6.0-dev.3](https://github.com/vtex/faststore/compare/v4.6.0-dev.2...v4.6.0-dev.3) (2026-08-12) + +**Note:** Version bump only for package @faststore/sdk + +# [4.6.0-dev.2](https://github.com/vtex/faststore/compare/v4.6.0-dev.1...v4.6.0-dev.2) (2026-08-12) + +**Note:** Version bump only for package @faststore/sdk + +# [4.6.0-dev.1](https://github.com/vtex/faststore/compare/v4.6.0-dev.0...v4.6.0-dev.1) (2026-08-11) + +## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.5.1) (2026-08-11) + +**Note:** Version bump only for package @faststore/sdk + +# [4.6.0-dev.0](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.6.0-dev.0) (2026-08-11) + +**Note:** Version bump only for package @faststore/sdk + +## [4.5.1-dev.4](https://github.com/vtex/faststore/compare/v4.5.1-dev.3...v4.5.1-dev.4) (2026-08-10) + +**Note:** Version bump only for package @faststore/sdk + +## [4.5.1-dev.3](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1-dev.3) (2026-08-10) + ## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1) (2026-08-11) **Note:** Version bump only for package @faststore/sdk diff --git a/packages/sdk/package.json b/packages/sdk/package.json index ebf1d6121e..613a57e7d6 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/sdk", - "version": "4.5.1", + "version": "4.6.0-dev.6", "description": "Hooks for creating your next component library", "license": "MIT", "repository": { diff --git a/packages/storybook/CHANGELOG.md b/packages/storybook/CHANGELOG.md index 474052b92a..fbc2832237 100644 --- a/packages/storybook/CHANGELOG.md +++ b/packages/storybook/CHANGELOG.md @@ -3,6 +3,42 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.6.0-dev.6](https://github.com/vtex/faststore/compare/v4.6.0-dev.5...v4.6.0-dev.6) (2026-08-14) + +**Note:** Version bump only for package @faststore/storybook + +# [4.6.0-dev.5](https://github.com/vtex/faststore/compare/v4.6.0-dev.4...v4.6.0-dev.5) (2026-08-13) + +**Note:** Version bump only for package @faststore/storybook + +# [4.6.0-dev.4](https://github.com/vtex/faststore/compare/v4.6.0-dev.3...v4.6.0-dev.4) (2026-08-12) + +**Note:** Version bump only for package @faststore/storybook + +# [4.6.0-dev.3](https://github.com/vtex/faststore/compare/v4.6.0-dev.2...v4.6.0-dev.3) (2026-08-12) + +**Note:** Version bump only for package @faststore/storybook + +# [4.6.0-dev.2](https://github.com/vtex/faststore/compare/v4.6.0-dev.1...v4.6.0-dev.2) (2026-08-12) + +**Note:** Version bump only for package @faststore/storybook + +# [4.6.0-dev.1](https://github.com/vtex/faststore/compare/v4.6.0-dev.0...v4.6.0-dev.1) (2026-08-11) + +## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.5.1) (2026-08-11) + +**Note:** Version bump only for package @faststore/storybook + +# [4.6.0-dev.0](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.6.0-dev.0) (2026-08-11) + +**Note:** Version bump only for package @faststore/storybook + +## [4.5.1-dev.4](https://github.com/vtex/faststore/compare/v4.5.1-dev.3...v4.5.1-dev.4) (2026-08-10) + +**Note:** Version bump only for package @faststore/storybook + +## [4.5.1-dev.3](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1-dev.3) (2026-08-10) + ## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1) (2026-08-11) **Note:** Version bump only for package @faststore/storybook diff --git a/packages/storybook/package.json b/packages/storybook/package.json index ac85684a26..5720075815 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/storybook", - "version": "4.5.1", + "version": "4.6.0-dev.6", "private": true, "engines": { "node": ">=20" diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index b031de5d5d..f47290f828 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -3,6 +3,42 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.6.0-dev.6](https://github.com/vtex/faststore/compare/v4.6.0-dev.5...v4.6.0-dev.6) (2026-08-14) + +**Note:** Version bump only for package @faststore/ui + +# [4.6.0-dev.5](https://github.com/vtex/faststore/compare/v4.6.0-dev.4...v4.6.0-dev.5) (2026-08-13) + +**Note:** Version bump only for package @faststore/ui + +# [4.6.0-dev.4](https://github.com/vtex/faststore/compare/v4.6.0-dev.3...v4.6.0-dev.4) (2026-08-12) + +**Note:** Version bump only for package @faststore/ui + +# [4.6.0-dev.3](https://github.com/vtex/faststore/compare/v4.6.0-dev.2...v4.6.0-dev.3) (2026-08-12) + +**Note:** Version bump only for package @faststore/ui + +# [4.6.0-dev.2](https://github.com/vtex/faststore/compare/v4.6.0-dev.1...v4.6.0-dev.2) (2026-08-12) + +**Note:** Version bump only for package @faststore/ui + +# [4.6.0-dev.1](https://github.com/vtex/faststore/compare/v4.6.0-dev.0...v4.6.0-dev.1) (2026-08-11) + +## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.5.1) (2026-08-11) + +**Note:** Version bump only for package @faststore/ui + +# [4.6.0-dev.0](https://github.com/vtex/faststore/compare/v4.5.1-dev.4...v4.6.0-dev.0) (2026-08-11) + +**Note:** Version bump only for package @faststore/ui + +## [4.5.1-dev.4](https://github.com/vtex/faststore/compare/v4.5.1-dev.3...v4.5.1-dev.4) (2026-08-10) + +**Note:** Version bump only for package @faststore/ui + +## [4.5.1-dev.3](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1-dev.3) (2026-08-10) + ## [4.5.1](https://github.com/vtex/faststore/compare/v4.5.1-dev.2...v4.5.1) (2026-08-11) **Note:** Version bump only for package @faststore/ui diff --git a/packages/ui/package.json b/packages/ui/package.json index e889854e3d..802a9427ed 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@faststore/ui", - "version": "4.5.1", + "version": "4.6.0-dev.6", "description": "A lightweight, framework agnostic component library for React", "author": "emersonlaurentino", "license": "MIT", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d3f06f9fe..a8c164aab1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,8 +61,8 @@ catalogs: specifier: ^3.1.1 version: 3.1.4 "@vitest/coverage-v8": - specifier: 4.0.7 - version: 4.0.7 + specifier: 4.0.18 + version: 4.0.18 cookie: specifier: ^0.7.0 version: 0.7.2 @@ -100,8 +100,8 @@ catalogs: specifier: 4.5.4 version: 4.5.4 vitest: - specifier: 4.0.7 - version: 4.0.7 + specifier: 4.0.18 + version: 4.0.18 overrides: graphql: 16.11.0 @@ -222,7 +222,7 @@ importers: version: 2.16.0 "@vitest/coverage-v8": specifier: "catalog:" - version: 4.0.7(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) concurrently: specifier: ^6.2.1 version: 6.5.1 @@ -245,7 +245,7 @@ importers: specifier: "catalog:" version: 4.5.4(@types/node@25.3.0)(rollup@4.58.0)(typescript@5.9.3)(vite@7.1.5(@types/node@25.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) vitest: - specifier: ^4.0.7 + specifier: "catalog:" version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) packages/cli: @@ -328,7 +328,7 @@ importers: version: 20.19.13 "@vitest/coverage-v8": specifier: "catalog:" - version: 4.0.7(vitest@4.0.7(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) graphql: specifier: 16.11.0 version: 16.11.0 @@ -349,7 +349,7 @@ importers: version: 5.9.3 vitest: specifier: "catalog:" - version: 4.0.7(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) packages/components: dependencies: @@ -386,7 +386,7 @@ importers: version: 3.1.4 "@vitest/coverage-v8": specifier: "catalog:" - version: 4.0.7(vitest@4.0.7(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) papaparse: specifier: ^5.5.3 version: 5.5.3 @@ -407,10 +407,10 @@ importers: version: 4.5.4(@types/node@25.3.0)(rollup@4.58.0)(typescript@5.9.3)(vite@7.1.5(@types/node@25.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: "catalog:" - version: 4.0.7(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) vitest-axe: specifier: ^0.1.0 - version: 0.1.0(vitest@4.0.7(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 0.1.0(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) packages/core: dependencies: @@ -615,7 +615,7 @@ importers: version: 5.1.4(vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) "@vitest/coverage-v8": specifier: "catalog:" - version: 4.0.7(vitest@4.0.7(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) axe-core: specifier: ^4.8.1 version: 4.11.1 @@ -645,22 +645,43 @@ importers: version: 5.9.3 vitest: specifier: "catalog:" - version: 4.0.7(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) packages/diagnostics: dependencies: + "@grpc/grpc-js": + specifier: ^1.14.4 + version: 1.14.4 "@opentelemetry/api": specifier: ^1.9.0 version: 1.9.0 + "@opentelemetry/api-logs": + specifier: ^0.219.0 + version: 0.219.0 + "@opentelemetry/exporter-logs-otlp-grpc": + specifier: ^0.219.0 + version: 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-trace-otlp-grpc": + specifier: ^0.219.0 + version: 0.219.0(@opentelemetry/api@1.9.0) "@opentelemetry/instrumentation-http": - specifier: ^0.212.0 - version: 0.212.0(@opentelemetry/api@1.9.0) - "@vtex/diagnostics-nodejs": - specifier: ^5.1.1 - version: 5.1.1(@opentelemetry/api@1.9.0)(@types/express@4.17.25)(express@4.22.1) - resolve-pkg: - specifier: ^3.0.1 - version: 3.0.1 + specifier: ^0.219.0 + version: 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/instrumentation-undici": + specifier: ^0.29.0 + version: 0.29.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": + specifier: ^2.8.0 + version: 2.9.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-logs": + specifier: ^0.219.0 + version: 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-node": + specifier: ^0.219.0 + version: 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-trace-node": + specifier: ^2.8.0 + version: 2.9.0(@opentelemetry/api@1.9.0) devDependencies: "@types/node": specifier: "catalog:" @@ -677,6 +698,9 @@ importers: vite-plugin-dts: specifier: "catalog:" version: 4.5.4(@types/node@20.19.13)(rollup@4.58.0)(typescript@5.9.3)(vite@7.1.5(@types/node@20.19.13)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + vitest: + specifier: "catalog:" + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) packages/lighthouse: devDependencies: @@ -710,7 +734,7 @@ importers: version: 18.3.28 "@vitest/coverage-v8": specifier: "catalog:" - version: 4.0.7(vitest@4.0.7(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) fake-indexeddb: specifier: ^3.1.3 version: 3.1.8 @@ -734,7 +758,7 @@ importers: version: 4.5.4(@types/node@20.19.13)(rollup@4.58.0)(typescript@5.9.3)(vite@7.1.5(@types/node@20.19.13)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) vitest: specifier: "catalog:" - version: 4.0.7(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) packages/storybook: dependencies: @@ -1334,6 +1358,13 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/helper-string-parser@7.29.7": + resolution: + { + integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==, + } + engines: { node: ">=6.9.0" } + "@babel/helper-validator-identifier@7.28.5": resolution: { @@ -1341,6 +1372,13 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/helper-validator-identifier@7.29.7": + resolution: + { + integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==, + } + engines: { node: ">=6.9.0" } + "@babel/helper-validator-option@7.27.1": resolution: { @@ -1370,6 +1408,14 @@ packages: engines: { node: ">=6.0.0" } hasBin: true + "@babel/parser@7.29.8": + resolution: + { + integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==, + } + engines: { node: ">=6.0.0" } + hasBin: true + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5": resolution: { @@ -2079,6 +2125,13 @@ packages: } engines: { node: ">=6.9.0" } + "@babel/types@7.29.8": + resolution: + { + integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==, + } + engines: { node: ">=6.9.0" } + "@bcoe/v8-coverage@1.0.2": resolution: { @@ -3501,17 +3554,17 @@ packages: } engines: { node: ">=18.0.0" } - "@grpc/grpc-js@1.14.3": + "@grpc/grpc-js@1.14.4": resolution: { - integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==, + integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==, } engines: { node: ">=12.10.0" } - "@grpc/proto-loader@0.8.0": + "@grpc/proto-loader@0.8.1": resolution: { - integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==, + integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==, } engines: { node: ">=6" } hasBin: true @@ -4792,27 +4845,13 @@ packages: integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==, } - "@opentelemetry/api-logs@0.200.0": - resolution: - { - integrity: sha512-IKJBQxh91qJ+3ssRly5hYEJ8NDHu9oY/B1PXVSCWf7zytmYO9RNLB0Ox9XQ/fJ8m6gY6Q6NtBWlmXfaXt5Uc4Q==, - } - engines: { node: ">=8.0.0" } - - "@opentelemetry/api-logs@0.212.0": + "@opentelemetry/api-logs@0.219.0": resolution: { - integrity: sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==, + integrity: sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==, } engines: { node: ">=8.0.0" } - "@opentelemetry/api-logs@0.57.2": - resolution: - { - integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==, - } - engines: { node: ">=14" } - "@opentelemetry/api@1.9.0": resolution: { @@ -4820,46 +4859,28 @@ packages: } engines: { node: ">=8.0.0" } - "@opentelemetry/baggage-span-processor@0.4.2": + "@opentelemetry/configuration@0.219.0": resolution: { - integrity: sha512-chJ9dJ3QSGZeqM71C95vqCtGpHru1+2OF802zW5D0FrpfzOgp2uJ0OdhA/ab2y0IOFbsH7xYu1tKGr7xV40vNw==, + integrity: sha512-wXZUYv4ngu43nA4WEhuXNacm46LW+17LRM8nKyIhBzroRA24PBYjMnakwzR/w777nFUB5xlgsYTTeuXxumZM1Q==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: - "@opentelemetry/api": ^1.0.0 + "@opentelemetry/api": ^1.9.0 - "@opentelemetry/context-async-hooks@2.0.0": + "@opentelemetry/context-async-hooks@2.8.0": resolution: { - integrity: sha512-IEkJGzK1A9v3/EHjXh3s2IiFc6L4jfK+lNgKVgUjeUJQRRhnVFMIO3TAvKwonm9O1HebCuoOt98v8bZW7oVQHA==, + integrity: sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - "@opentelemetry/context-async-hooks@2.5.1": + "@opentelemetry/context-async-hooks@2.9.0": resolution: { - integrity: sha512-MHbu8XxCHcBn6RwvCt2Vpn1WnLMNECfNKYB14LI5XypcgH4IE0/DiVifVR9tAkwPMyLXN8dOoPJfya3IryLQVw==, - } - engines: { node: ^18.19.0 || >=20.6.0 } - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - - "@opentelemetry/core@1.30.1": - resolution: - { - integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==, - } - engines: { node: ">=14" } - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - - "@opentelemetry/core@2.0.0": - resolution: - { - integrity: sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ==, + integrity: sha512-OQ0vzvbZBiUhjqLnUaoNfYmP8553Crr3aggB4y0ZUi815mZ7idpdJXQmoKdeBKJelYttoBlLSSHubmyw3wvX4w==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: @@ -4874,327 +4895,284 @@ packages: peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - "@opentelemetry/exporter-logs-otlp-grpc@0.200.0": + "@opentelemetry/core@2.8.0": resolution: { - integrity: sha512-+3MDfa5YQPGM3WXxW9kqGD85Q7s9wlEMVNhXXG7tYFLnIeaseUt9YtCeFhEDFzfEktacdFpOtXmJuNW8cHbU5A==, + integrity: sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: - "@opentelemetry/api": ^1.3.0 + "@opentelemetry/api": ">=1.0.0 <1.10.0" - "@opentelemetry/exporter-logs-otlp-http@0.200.0": + "@opentelemetry/core@2.9.0": resolution: { - integrity: sha512-KfWw49htbGGp9s8N4KI8EQ9XuqKJ0VG+yVYVYFiCYSjEV32qpQ5qZ9UZBzOZ6xRb+E16SXOSCT3RkqBVSABZ+g==, + integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: - "@opentelemetry/api": ^1.3.0 + "@opentelemetry/api": ">=1.0.0 <1.10.0" - "@opentelemetry/exporter-logs-otlp-proto@0.200.0": + "@opentelemetry/exporter-logs-otlp-grpc@0.219.0": resolution: { - integrity: sha512-GmahpUU/55hxfH4TP77ChOfftADsCq/nuri73I/AVLe2s4NIglvTsaACkFVZAVmnXXyPS00Fk3x27WS3yO07zA==, + integrity: sha512-7SvzDCIclHWAcCwZ1MTOLcwn4BVNPGI3QxS/DJraPNe1TTL+4TvUBq5zeQV8tsnYvtDN7wKW2qocVmaCP2l7sQ==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/exporter-metrics-otlp-grpc@0.200.0": + "@opentelemetry/exporter-logs-otlp-http@0.219.0": resolution: { - integrity: sha512-uHawPRvKIrhqH09GloTuYeq2BjyieYHIpiklOvxm9zhrCL2eRsnI/6g9v2BZTVtGp8tEgIa7rCQ6Ltxw6NBgew==, + integrity: sha512-mhl2HL6GmZI8b8PwPfqMws/5ovJfbRTxwc9Y5agVVHiQ+e5SL1btsFr/kJDgt7YCexDtsUn5HAreHQO9szFS0A==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/exporter-metrics-otlp-http@0.200.0": + "@opentelemetry/exporter-logs-otlp-proto@0.219.0": resolution: { - integrity: sha512-5BiR6i8yHc9+qW7F6LqkuUnIzVNA7lt0qRxIKcKT+gq3eGUPHZ3DY29sfxI3tkvnwMgtnHDMNze5DdxW39HsAw==, + integrity: sha512-Ayw4Gf71PS9jhBVaYywa4WsajnqfDehMkTdVH3TSAVHqPcsAv/AhH/wTNRYNt99szeYr6Gbd/D6RjZD77wAxHg==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/exporter-metrics-otlp-proto@0.200.0": + "@opentelemetry/exporter-metrics-otlp-grpc@0.219.0": resolution: { - integrity: sha512-E+uPj0yyvz81U9pvLZp3oHtFrEzNSqKGVkIViTQY1rH3TOobeJPSpLnTVXACnCwkPR5XeTvPnK3pZ2Kni8AFMg==, + integrity: sha512-6LaaSrPxK5L55bXevWajvOMxGOpNm0n12tG53TeZaUeNzXwLPg6d2KCC1zAlGsojan+xRG71mA4Qqs9K2VVrKQ==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/exporter-prometheus@0.200.0": + "@opentelemetry/exporter-metrics-otlp-http@0.219.0": resolution: { - integrity: sha512-ZYdlU9r0USuuYppiDyU2VFRA0kFl855ylnb3N/2aOlXrbA4PMCznen7gmPbetGQu7pz8Jbaf4fwvrDnVdQQXSw==, + integrity: sha512-6CaDRbMVHZSDWzNXwrR8y/H4B/Z1eMNnkHiPQlTx3Ojz2OHY4X/aff/UC4P/3pHUQSuTfi3oh2UsPPZppw+Vrg==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/exporter-trace-otlp-grpc@0.200.0": + "@opentelemetry/exporter-metrics-otlp-proto@0.219.0": resolution: { - integrity: sha512-hmeZrUkFl1YMsgukSuHCFPYeF9df0hHoKeHUthRKFCxiURs+GwF1VuabuHmBMZnjTbsuvNjOB+JSs37Csem/5Q==, + integrity: sha512-DUS7XyIiEnoeccQUvuKy0G2/YqeKhpN8FVIrGbrLNIVMj10yeIFLRzRv0tibCI2kXXvlTTABVexGAk78wHk2ug==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/exporter-trace-otlp-http@0.200.0": + "@opentelemetry/exporter-prometheus@0.219.0": resolution: { - integrity: sha512-Goi//m/7ZHeUedxTGVmEzH19NgqJY+Bzr6zXo1Rni1+hwqaksEyJ44gdlEMREu6dzX1DlAaH/qSykSVzdrdafA==, + integrity: sha512-TxOnJ85eWJY5JyOJsNMXiRTYlkDcOv0u3KbXEzWCc+tUS9sjL/BC6BcdxZ0B9r2OFVqsrZFXUzSD2sZUy42Ucw==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/exporter-trace-otlp-proto@0.200.0": + "@opentelemetry/exporter-trace-otlp-grpc@0.219.0": resolution: { - integrity: sha512-V9TDSD3PjK1OREw2iT9TUTzNYEVWJk4Nhodzhp9eiz4onDMYmPy3LaGbPv81yIR6dUb/hNp/SIhpiCHwFUq2Vg==, + integrity: sha512-BkDNv1UD6BscW19MxbAxVmSYSSFuyeqR6buV2/HTYqA7GrR0EbTFzqG6h86T3PtXmpdbsWjMGLDdjG2rikG27Q==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/exporter-zipkin@2.0.0": + "@opentelemetry/exporter-trace-otlp-http@0.219.0": resolution: { - integrity: sha512-icxaKZ+jZL/NHXX8Aru4HGsrdhK0MLcuRXkX5G5IRmCgoRLw+Br6I/nMVozX2xjGGwV7hw2g+4Slj8K7s4HbVg==, + integrity: sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw==, } engines: { node: ^18.19.0 || >=20.6.0 } - peerDependencies: - "@opentelemetry/api": ^1.0.0 - - "@opentelemetry/instrumentation-express@0.47.1": - resolution: - { - integrity: sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==, - } - engines: { node: ">=14" } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/instrumentation-http@0.200.0": + "@opentelemetry/exporter-trace-otlp-proto@0.219.0": resolution: { - integrity: sha512-9tqGbCJikhYU68y3k9mi6yWsMyMeCcwoQuHvIXan5VvvPPQ5WIZaV6Mxu/MCVe4swRNoFs8Th+qyj0TZV5ELvw==, + integrity: sha512-lF/LUBfhOFmxJa+SQsLN7ziV4MHa2pyKgOM6JNehSOfU+npjM4gwm9oIKEJrzrWcexMcqydiyoFy0XCb1Ql3wQ==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/instrumentation-http@0.212.0": + "@opentelemetry/exporter-zipkin@2.8.0": resolution: { - integrity: sha512-t2nt16Uyv9irgR+tqnX96YeToOStc3X5js7Ljn3EKlI2b4Fe76VhMkTXtsTQ0aId6AsYgefrCRnXSCo/Fn/vww==, + integrity: sha512-Mj84UkEa17BK2o903VTXW3wM8CrSZexGs4tRGVZVIMM9ni1T6TuGx5IrRfoWKAbshx42D5/kc7YV+axypLPYyA==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: - "@opentelemetry/api": ^1.3.0 + "@opentelemetry/api": ^1.0.0 - "@opentelemetry/instrumentation-net@0.44.0": + "@opentelemetry/instrumentation-http@0.219.0": resolution: { - integrity: sha512-SmAbOKTi0lgdTN9XMXOaf+4jw670MpiK3pw9/to/kRlTvNWwWA4RD34trCcoL7Gf2IYoXuj56Oo4Z5C7N98ukw==, + integrity: sha512-nNt1fqpyah/OKjNHdEOu8xLwISppRU2qJuF8aR+fCcftVwdFkPgtworBLA+TI1HU2iF508jcQBF2gerWczJAXg==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/instrumentation@0.200.0": + "@opentelemetry/instrumentation-undici@0.29.0": resolution: { - integrity: sha512-pmPlzfJd+vvgaZd/reMsC8RWgTXn2WY1OWT5RT42m3aOn5532TozwXNDhg1vzqJ+jnvmkREcdLr27ebJEQt0Jg==, + integrity: sha512-SnA+0XgGc595jtnwFVfWy7Vgfr5hle4D5YKIlm0U4z8aK9YoCZVUn1xAkVZ2evaJyykiDF50FBzr1XZ0uj8CPA==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: - "@opentelemetry/api": ^1.3.0 + "@opentelemetry/api": ^1.7.0 - "@opentelemetry/instrumentation@0.212.0": + "@opentelemetry/instrumentation@0.219.0": resolution: { - integrity: sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==, + integrity: sha512-X5t7I8GyIO9rmGHwoedZLREpQqrF1WW2nxzNNym6HOKpFiE+rvqV3ngC0xcZVO2YwIGf3KKmRdWrYwdwz3H9RQ==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/instrumentation@0.57.2": + "@opentelemetry/otlp-exporter-base@0.219.0": resolution: { - integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==, - } - engines: { node: ">=14" } - peerDependencies: - "@opentelemetry/api": ^1.3.0 - - "@opentelemetry/otlp-exporter-base@0.200.0": - resolution: - { - integrity: sha512-IxJgA3FD7q4V6gGq4bnmQM5nTIyMDkoGFGrBrrDjB6onEiq1pafma55V+bHvGYLWvcqbBbRfezr1GED88lacEQ==, + integrity: sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/otlp-grpc-exporter-base@0.200.0": + "@opentelemetry/otlp-grpc-exporter-base@0.219.0": resolution: { - integrity: sha512-CK2S+bFgOZ66Bsu5hlDeOX6cvW5FVtVjFFbWuaJP0ELxJKBB6HlbLZQ2phqz/uLj1cWap5xJr/PsR3iGoB7Vqw==, + integrity: sha512-iIk/s8QQu39zpTrRRmsW/Eg3SE2+Hg8tLWepr2FLRgmwUpNd0IpCTLJEHJ77hpt4hgIS8MAh44UYI4xQPZwWlw==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/otlp-transformer@0.200.0": + "@opentelemetry/otlp-transformer@0.219.0": resolution: { - integrity: sha512-+9YDZbYybOnv7sWzebWOeK6gKyt2XE7iarSyBFkwwnP559pEevKOUD8NyDHhRjCSp13ybh9iVXlMfcj/DwF/yw==, + integrity: sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ^1.3.0 - "@opentelemetry/propagator-b3@2.0.0": + "@opentelemetry/propagator-b3@2.8.0": resolution: { - integrity: sha512-blx9S2EI49Ycuw6VZq+bkpaIoiJFhsDuvFGhBIoH3vJ5oYjJ2U0s3fAM5jYft99xVIAv6HqoPtlP9gpVA2IZtA==, + integrity: sha512-SazlvuSKi5533rPHTW2TwBwdMakhjZST4SYs0YauuvfGDkT13KbG1gJS75hV0uWVeevhtVP9sAIlaZLTHdSbMg==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - "@opentelemetry/propagator-jaeger@2.0.0": + "@opentelemetry/propagator-jaeger@2.8.0": resolution: { - integrity: sha512-Mbm/LSFyAtQKP0AQah4AfGgsD+vsZcyreZoQ5okFBk33hU7AquU4TltgyL9dvaO8/Zkoud8/0gEvwfOZ5d7EPA==, + integrity: sha512-Xnz9zZvvQzUw+9DrOn0MomR7BxFCkA2pcfXBQuHC28ndJpSbjLs7knzYb05kw5SyCjSsEWombkZMgGcJSk8JVg==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - "@opentelemetry/resource-detector-aws@1.12.0": + "@opentelemetry/resources@2.8.0": resolution: { - integrity: sha512-Cvi7ckOqiiuWlHBdA1IjS0ufr3sltex2Uws2RK6loVp4gzIJyOijsddAI6IZ5kiO8h/LgCWe8gxPmwkTKImd+Q==, - } - engines: { node: ">=14" } - peerDependencies: - "@opentelemetry/api": ^1.0.0 - - "@opentelemetry/resources@1.30.1": - resolution: - { - integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==, - } - engines: { node: ">=14" } - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - - "@opentelemetry/resources@2.0.0": - resolution: - { - integrity: sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg==, + integrity: sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ">=1.3.0 <1.10.0" - "@opentelemetry/resources@2.5.1": + "@opentelemetry/resources@2.9.0": resolution: { - integrity: sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==, + integrity: sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ">=1.3.0 <1.10.0" - "@opentelemetry/sdk-logs@0.200.0": + "@opentelemetry/sdk-logs@0.219.0": resolution: { - integrity: sha512-VZG870063NLfObmQQNtCVcdXXLzI3vOjjrRENmU37HYiPFa0ZXpXVDsTD02Nh3AT3xYJzQaWKl2X2lQ2l7TWJA==, + integrity: sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ">=1.4.0 <1.10.0" - "@opentelemetry/sdk-metrics@2.0.0": - resolution: - { - integrity: sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA==, - } - engines: { node: ^18.19.0 || >=20.6.0 } - peerDependencies: - "@opentelemetry/api": ">=1.9.0 <1.10.0" - - "@opentelemetry/sdk-metrics@2.5.1": + "@opentelemetry/sdk-metrics@2.8.0": resolution: { - integrity: sha512-RKMn3QKi8nE71ULUo0g/MBvq1N4icEBo7cQSKnL3URZT16/YH3nSVgWegOjwx7FRBTrjOIkMJkCUn/ZFIEfn4A==, + integrity: sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ">=1.9.0 <1.10.0" - "@opentelemetry/sdk-node@0.200.0": + "@opentelemetry/sdk-node@0.219.0": resolution: { - integrity: sha512-S/YSy9GIswnhYoDor1RusNkmRughipvTCOQrlF1dzI70yQaf68qgf5WMnzUxdlCl3/et/pvaO75xfPfuEmCK5A==, + integrity: sha512-NWLpWLEb8gV3+JBHYoIrktbM385wyHpRJoh3J/4Q52d4PR+AlPMNGJT3DzBUrDSUEVbKAXoHR+EDAPxtiNcj8g==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ">=1.3.0 <1.10.0" - "@opentelemetry/sdk-trace-base@2.0.0": + "@opentelemetry/sdk-trace-base@2.8.0": resolution: { - integrity: sha512-qQnYdX+ZCkonM7tA5iU4fSRsVxbFGml8jbxOgipRGMFHKaXKHQ30js03rTobYjKjIfnOsZSbHKWF0/0v0OQGfw==, + integrity: sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ">=1.3.0 <1.10.0" - "@opentelemetry/sdk-trace-base@2.5.1": + "@opentelemetry/sdk-trace-base@2.9.0": resolution: { - integrity: sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==, + integrity: sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ">=1.3.0 <1.10.0" - "@opentelemetry/sdk-trace-node@2.0.0": + "@opentelemetry/sdk-trace-node@2.8.0": resolution: { - integrity: sha512-omdilCZozUjQwY3uZRBwbaRMJ3p09l4t187Lsdf0dGMye9WKD4NGcpgZRvqhI1dwcH6og+YXQEtoO9Wx3ykilg==, + integrity: sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - "@opentelemetry/sdk-trace-node@2.5.1": + "@opentelemetry/sdk-trace-node@2.9.0": resolution: { - integrity: sha512-9lopQ6ZoElETOEN0csgmtEV5/9C7BMfA7VtF4Jape3i954b6sTY2k3Xw3CxUTKreDck/vpAuJM+EDo4zheUw+A==, + integrity: sha512-ec9a7ps37huy5itYk0MalaZdSLlM6AXWp/FhtEjgMpp5leEGojBDvAl/UWttQnkMZOvFHKzRESn8TD3yKTF5nQ==, } engines: { node: ^18.19.0 || >=20.6.0 } peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - "@opentelemetry/semantic-conventions@1.28.0": + "@opentelemetry/sdk-trace@2.9.0": resolution: { - integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==, + integrity: sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==, } - engines: { node: ">=14" } + engines: { node: ^18.19.0 || >=20.6.0 } + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.10.0" "@opentelemetry/semantic-conventions@1.40.0": resolution: @@ -5395,22 +5373,22 @@ packages: integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==, } - "@protobufjs/codegen@2.0.4": + "@protobufjs/codegen@2.0.5": resolution: { - integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==, + integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==, } - "@protobufjs/eventemitter@1.1.0": + "@protobufjs/eventemitter@1.1.1": resolution: { - integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==, + integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==, } - "@protobufjs/fetch@1.1.0": + "@protobufjs/fetch@1.1.1": resolution: { - integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==, + integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==, } "@protobufjs/float@1.0.2": @@ -5419,12 +5397,6 @@ packages: integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==, } - "@protobufjs/inquire@1.1.0": - resolution: - { - integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==, - } - "@protobufjs/path@1.1.2": resolution: { @@ -5437,10 +5409,10 @@ packages: integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==, } - "@protobufjs/utf8@1.1.0": + "@protobufjs/utf8@1.1.2": resolution: { - integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==, + integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==, } "@puppeteer/browsers@2.13.0": @@ -7088,12 +7060,6 @@ packages: integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==, } - "@types/shimmer@1.2.0": - resolution: - { - integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==, - } - "@types/sinonjs__fake-timers@8.1.1": resolution: { @@ -7145,14 +7111,14 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - "@vitest/coverage-v8@4.0.7": + "@vitest/coverage-v8@4.0.18": resolution: { - integrity: sha512-MXc+kEA5EUwMMGmNt1S6CIOEl/iCmAhGZQq1QgMNC3/QpYSOxkysEi6pxWhkqJ7YT/RduoVEV5rxFxHG18V3LA==, + integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==, } peerDependencies: - "@vitest/browser": 4.0.7 - vitest: 4.0.7 + "@vitest/browser": 4.0.18 + vitest: 4.0.18 peerDependenciesMeta: "@vitest/browser": optional: true @@ -7175,12 +7141,6 @@ packages: integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==, } - "@vitest/expect@4.0.7": - resolution: - { - integrity: sha512-jGRG6HghnJDjljdjYIoVzX17S6uCVCBRFnsgdLGJ6CaxfPh8kzUKe/2n533y4O/aeZ/sIr7q7GbuEbeGDsWv4Q==, - } - "@vitest/mocker@4.0.18": resolution: { @@ -7195,20 +7155,6 @@ packages: vite: optional: true - "@vitest/mocker@4.0.7": - resolution: - { - integrity: sha512-OsDwLS7WnpuNslOV6bJkXVYVV/6RSc4eeVxV7h9wxQPNxnjRvTTrIikfwCbMyl8XJmW6oOccBj2Q07YwZtQcCw==, - } - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - "@vitest/pretty-format@2.0.5": resolution: { @@ -7233,36 +7179,18 @@ packages: integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==, } - "@vitest/pretty-format@4.0.7": - resolution: - { - integrity: sha512-YY//yxqTmk29+/pK+Wi1UB4DUH3lSVgIm+M10rAJ74pOSMgT7rydMSc+vFuq9LjZLhFvVEXir8EcqMke3SVM6Q==, - } - "@vitest/runner@4.0.18": resolution: { integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==, } - "@vitest/runner@4.0.7": - resolution: - { - integrity: sha512-orU1lsu4PxLEcDWfjVCNGIedOSF/YtZ+XMrd1PZb90E68khWCNzD8y1dtxtgd0hyBIQk8XggteKN/38VQLvzuw==, - } - "@vitest/snapshot@4.0.18": resolution: { integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==, } - "@vitest/snapshot@4.0.7": - resolution: - { - integrity: sha512-xJL+Nkw0OjaUXXQf13B8iKK5pI9QVtN9uOtzNHYuG/o/B7fIEg0DQ+xOe0/RcqwDEI15rud1k7y5xznBKGUXAA==, - } - "@vitest/spy@2.0.5": resolution: { @@ -7281,12 +7209,6 @@ packages: integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==, } - "@vitest/spy@4.0.7": - resolution: - { - integrity: sha512-FW4X8hzIEn4z+HublB4hBF/FhCVaXfIHm8sUfvlznrcy1MQG7VooBgZPMtVCGZtHi0yl3KESaXTqsKh16d8cFg==, - } - "@vitest/utils@2.0.5": resolution: { @@ -7311,12 +7233,6 @@ packages: integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==, } - "@vitest/utils@4.0.7": - resolution: - { - integrity: sha512-HNrg9CM/Z4ZWB6RuExhuC6FPmLipiShKVMnT9JlQvfhwR47JatWLChA6mtZqVHqypE6p/z6ofcjbyWpM7YLxPQ==, - } - "@volar/language-core@2.4.28": resolution: { @@ -7348,38 +7264,6 @@ packages: } engines: { node: ">=16" } - "@vtex/diagnostics-nodejs@5.1.1": - resolution: - { - integrity: sha512-f6m/2zO2X59kGL/uljKECsOsHl7StQ7CPfaTl9PQJZ4rx2hGg0t0RERek9m9fwg5lmJwrpeoerX4bRqiU7r2/A==, - } - engines: { node: ">=14.0.0" } - peerDependencies: - "@opentelemetry/api": ">=1.0.0" - "@types/express": "*" - "@types/koa": "*" - express: ^4.0.0 || ^5.0.0 - fastify: ^4.0.0 - koa: ^2.0.0 - peerDependenciesMeta: - "@types/express": - optional: true - "@types/koa": - optional: true - express: - optional: true - fastify: - optional: true - koa: - optional: true - - "@vtex/diagnostics-semconv@5.1.0": - resolution: - { - integrity: sha512-apTPplntetN0y7+A0b6+9qh7SRv5Q7Ox+bQ7e3bY84UBWM6uBrphWeHNJve9DI3H9Y8oA1jjZCtXNamiE7hiWA==, - } - engines: { node: ">=14" } - "@vtex/faststore-sdk@0.1.1-canary.5": resolution: { @@ -7634,14 +7518,6 @@ packages: } engines: { node: ">= 0.6" } - acorn-import-attributes@1.9.5: - resolution: - { - integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==, - } - peerDependencies: - acorn: ^8 - acorn-import-phases@1.0.4: resolution: { @@ -10325,6 +10201,12 @@ packages: integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==, } + es-module-lexer@2.3.1: + resolution: + { + integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==, + } + es-object-atoms@1.1.1: resolution: { @@ -12013,17 +11895,12 @@ packages: } engines: { node: ">=12.2" } - import-in-the-middle@1.15.0: + import-in-the-middle@3.3.2: resolution: { - integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==, - } - - import-in-the-middle@2.0.6: - resolution: - { - integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==, + integrity: sha512-jTd2FfOgOWOdgjkHuk/1Ms8VKFXkPs15ymYBETw1sAOrO/dY3XeGVRWir9qBbw7pXr0T2eTFwfCZ+N02HmiNGA==, } + engines: { node: ">=18" } import-lazy@4.0.0: resolution: @@ -12687,13 +12564,6 @@ packages: } engines: { node: ">=10" } - istanbul-lib-source-maps@5.0.6: - resolution: - { - integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==, - } - engines: { node: ">=10" } - istanbul-reports@3.2.0: resolution: { @@ -13388,10 +13258,10 @@ packages: integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, } - magicast@0.3.5: + magicast@0.5.4: resolution: { - integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==, + integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==, } make-dir@2.1.0: @@ -15282,10 +15152,10 @@ packages: integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==, } - protobufjs@7.5.4: + protobufjs@7.6.5: resolution: { - integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==, + integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==, } engines: { node: ">=12.0.0" } @@ -15781,13 +15651,6 @@ packages: } engines: { node: ">=0.10.0" } - require-in-the-middle@7.5.2: - resolution: - { - integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==, - } - engines: { node: ">=8.6.0" } - require-in-the-middle@8.0.1: resolution: { @@ -16290,12 +16153,6 @@ packages: engines: { node: ">=4" } hasBin: true - shimmer@1.2.1: - resolution: - { - integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==, - } - shx@0.3.4: resolution: { @@ -18114,43 +17971,6 @@ packages: jsdom: optional: true - vitest@4.0.7: - resolution: - { - integrity: sha512-xQroKAadK503CrmbzCISvQUjeuvEZzv6U0wlnlVFOi5i3gnzfH4onyQ29f3lzpe0FresAiTAd3aqK0Bi/jLI8w==, - } - engines: { node: ^20.0.0 || ^22.0.0 || >=24.0.0 } - hasBin: true - peerDependencies: - "@edge-runtime/vm": "*" - "@types/debug": ^4.1.12 - "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.0.7 - "@vitest/browser-preview": 4.0.7 - "@vitest/browser-webdriverio": 4.0.7 - "@vitest/ui": 4.0.7 - happy-dom: "*" - jsdom: "*" - peerDependenciesMeta: - "@edge-runtime/vm": - optional: true - "@types/debug": - optional: true - "@types/node": - optional: true - "@vitest/browser-playwright": - optional: true - "@vitest/browser-preview": - optional: true - "@vitest/browser-webdriverio": - optional: true - "@vitest/ui": - optional: true - happy-dom: - optional: true - jsdom: - optional: true - vm-browserify@1.1.2: resolution: { @@ -19416,8 +19236,12 @@ snapshots: "@babel/helper-string-parser@7.27.1": {} + "@babel/helper-string-parser@7.29.7": {} + "@babel/helper-validator-identifier@7.28.5": {} + "@babel/helper-validator-identifier@7.29.7": {} + "@babel/helper-validator-option@7.27.1": {} "@babel/helper-wrap-function@7.28.6": @@ -19437,6 +19261,10 @@ snapshots: dependencies: "@babel/types": 7.29.0 + "@babel/parser@7.29.8": + dependencies: + "@babel/types": 7.29.8 + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)": dependencies: "@babel/core": 7.29.0 @@ -20038,6 +19866,11 @@ snapshots: "@babel/helper-string-parser": 7.27.1 "@babel/helper-validator-identifier": 7.28.5 + "@babel/types@7.29.8": + dependencies: + "@babel/helper-string-parser": 7.29.7 + "@babel/helper-validator-identifier": 7.29.7 + "@bcoe/v8-coverage@1.0.2": {} "@biomejs/biome@1.9.4": @@ -21279,16 +21112,16 @@ snapshots: "@repeaterjs/repeater": 3.0.6 tslib: 2.8.1 - "@grpc/grpc-js@1.14.3": + "@grpc/grpc-js@1.14.4": dependencies: - "@grpc/proto-loader": 0.8.0 + "@grpc/proto-loader": 0.8.1 "@js-sdsl/ordered-map": 4.4.2 - "@grpc/proto-loader@0.8.0": + "@grpc/proto-loader@0.8.1": dependencies: lodash.camelcase: 4.3.0 long: 5.3.2 - protobufjs: 7.5.4 + protobufjs: 7.6.5 yargs: 17.7.2 "@hutson/parse-repository-url@3.0.2": {} @@ -22481,358 +22314,302 @@ snapshots: dependencies: "@octokit/openapi-types": 24.2.0 - "@opentelemetry/api-logs@0.200.0": - dependencies: - "@opentelemetry/api": 1.9.0 - - "@opentelemetry/api-logs@0.212.0": - dependencies: - "@opentelemetry/api": 1.9.0 - - "@opentelemetry/api-logs@0.57.2": + "@opentelemetry/api-logs@0.219.0": dependencies: "@opentelemetry/api": 1.9.0 "@opentelemetry/api@1.9.0": {} - "@opentelemetry/baggage-span-processor@0.4.2(@opentelemetry/api@1.9.0)": - dependencies: - "@opentelemetry/api": 1.9.0 - "@opentelemetry/sdk-trace-base": 2.5.1(@opentelemetry/api@1.9.0) - - "@opentelemetry/context-async-hooks@2.0.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/configuration@0.219.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + yaml: 2.8.2 - "@opentelemetry/context-async-hooks@2.5.1(@opentelemetry/api@1.9.0)": + "@opentelemetry/context-async-hooks@2.8.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)": + "@opentelemetry/context-async-hooks@2.9.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/semantic-conventions": 1.28.0 - "@opentelemetry/core@2.0.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 "@opentelemetry/semantic-conventions": 1.40.0 - "@opentelemetry/core@2.5.1(@opentelemetry/api@1.9.0)": + "@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 "@opentelemetry/semantic-conventions": 1.40.0 - "@opentelemetry/exporter-logs-otlp-grpc@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.0)": dependencies: - "@grpc/grpc-js": 1.14.3 "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-exporter-base": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-grpc-exporter-base": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-transformer": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-logs": 0.200.0(@opentelemetry/api@1.9.0) + "@opentelemetry/semantic-conventions": 1.40.0 - "@opentelemetry/exporter-logs-otlp-http@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/exporter-logs-otlp-grpc@0.219.0(@opentelemetry/api@1.9.0)": dependencies: + "@grpc/grpc-js": 1.14.4 "@opentelemetry/api": 1.9.0 - "@opentelemetry/api-logs": 0.200.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-exporter-base": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-transformer": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-logs": 0.200.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-grpc-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-transformer": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-logs": 0.219.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-logs-otlp-proto@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/exporter-logs-otlp-http@0.219.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/api-logs": 0.200.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-exporter-base": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-transformer": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-logs": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-trace-base": 2.0.0(@opentelemetry/api@1.9.0) + "@opentelemetry/api-logs": 0.219.0 + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-transformer": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-logs": 0.219.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-metrics-otlp-grpc@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/exporter-logs-otlp-proto@0.219.0(@opentelemetry/api@1.9.0)": dependencies: - "@grpc/grpc-js": 1.14.3 "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-metrics-otlp-http": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-exporter-base": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-grpc-exporter-base": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-transformer": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-metrics": 2.0.0(@opentelemetry/api@1.9.0) + "@opentelemetry/api-logs": 0.219.0 + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-transformer": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-logs": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-trace-base": 2.8.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-metrics-otlp-http@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/exporter-metrics-otlp-grpc@0.219.0(@opentelemetry/api@1.9.0)": dependencies: + "@grpc/grpc-js": 1.14.4 "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-exporter-base": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-transformer": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-metrics": 2.0.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-metrics-otlp-http": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-grpc-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-transformer": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-metrics": 2.8.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-metrics-otlp-proto@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/exporter-metrics-otlp-http@0.219.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-metrics-otlp-http": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-exporter-base": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-transformer": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-metrics": 2.0.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-transformer": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-metrics": 2.8.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-prometheus@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/exporter-metrics-otlp-proto@0.219.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-metrics": 2.0.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-metrics-otlp-http": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-transformer": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-metrics": 2.8.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-trace-otlp-grpc@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/exporter-prometheus@0.219.0(@opentelemetry/api@1.9.0)": dependencies: - "@grpc/grpc-js": 1.14.3 "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-exporter-base": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-grpc-exporter-base": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-transformer": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-trace-base": 2.0.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-metrics": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/semantic-conventions": 1.40.0 - "@opentelemetry/exporter-trace-otlp-http@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/exporter-trace-otlp-grpc@0.219.0(@opentelemetry/api@1.9.0)": dependencies: + "@grpc/grpc-js": 1.14.4 "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-exporter-base": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-transformer": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-trace-base": 2.0.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-grpc-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-transformer": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-trace-base": 2.8.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-trace-otlp-proto@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/exporter-trace-otlp-http@0.219.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-exporter-base": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-transformer": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-trace-base": 2.0.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-transformer": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-trace-base": 2.8.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-zipkin@2.0.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/exporter-trace-otlp-proto@0.219.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-trace-base": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/semantic-conventions": 1.40.0 + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-transformer": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-trace-base": 2.8.0(@opentelemetry/api@1.9.0) - "@opentelemetry/instrumentation-express@0.47.1(@opentelemetry/api@1.9.0)": + "@opentelemetry/exporter-zipkin@2.8.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 1.30.1(@opentelemetry/api@1.9.0) - "@opentelemetry/instrumentation": 0.57.2(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-trace-base": 2.8.0(@opentelemetry/api@1.9.0) "@opentelemetry/semantic-conventions": 1.40.0 - transitivePeerDependencies: - - supports-color - "@opentelemetry/instrumentation-http@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/instrumentation-http@0.219.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/instrumentation": 0.200.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/instrumentation": 0.219.0(@opentelemetry/api@1.9.0) "@opentelemetry/semantic-conventions": 1.40.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color - "@opentelemetry/instrumentation-http@0.212.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/instrumentation-undici@0.29.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 "@opentelemetry/core": 2.5.1(@opentelemetry/api@1.9.0) - "@opentelemetry/instrumentation": 0.212.0(@opentelemetry/api@1.9.0) + "@opentelemetry/instrumentation": 0.219.0(@opentelemetry/api@1.9.0) "@opentelemetry/semantic-conventions": 1.40.0 - forwarded-parse: 2.1.2 - transitivePeerDependencies: - - supports-color - - "@opentelemetry/instrumentation-net@0.44.0(@opentelemetry/api@1.9.0)": - dependencies: - "@opentelemetry/api": 1.9.0 - "@opentelemetry/instrumentation": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/semantic-conventions": 1.40.0 - transitivePeerDependencies: - - supports-color - - "@opentelemetry/instrumentation@0.200.0(@opentelemetry/api@1.9.0)": - dependencies: - "@opentelemetry/api": 1.9.0 - "@opentelemetry/api-logs": 0.200.0 - "@types/shimmer": 1.2.0 - import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 - shimmer: 1.2.1 transitivePeerDependencies: - supports-color - "@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/instrumentation@0.219.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/api-logs": 0.212.0 - import-in-the-middle: 2.0.6 + "@opentelemetry/api-logs": 0.219.0 + import-in-the-middle: 3.3.2 require-in-the-middle: 8.0.1 transitivePeerDependencies: - supports-color - "@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)": + "@opentelemetry/otlp-exporter-base@0.219.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/api-logs": 0.57.2 - "@types/shimmer": 1.2.0 - import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 - semver: 7.7.4 - shimmer: 1.2.1 - transitivePeerDependencies: - - supports-color + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-transformer": 0.219.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-exporter-base@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/otlp-grpc-exporter-base@0.219.0(@opentelemetry/api@1.9.0)": dependencies: + "@grpc/grpc-js": 1.14.4 "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-transformer": 0.200.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-transformer": 0.219.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-grpc-exporter-base@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/otlp-transformer@0.219.0(@opentelemetry/api@1.9.0)": dependencies: - "@grpc/grpc-js": 1.14.3 "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-exporter-base": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-transformer": 0.200.0(@opentelemetry/api@1.9.0) + "@opentelemetry/api-logs": 0.219.0 + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-logs": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-metrics": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-trace-base": 2.8.0(@opentelemetry/api@1.9.0) - "@opentelemetry/otlp-transformer@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/propagator-b3@2.8.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/api-logs": 0.200.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-logs": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-metrics": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-trace-base": 2.0.0(@opentelemetry/api@1.9.0) - protobufjs: 7.5.4 + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) - "@opentelemetry/propagator-b3@2.0.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/propagator-jaeger@2.8.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) - "@opentelemetry/propagator-jaeger@2.0.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - - "@opentelemetry/resource-detector-aws@1.12.0(@opentelemetry/api@1.9.0)": - dependencies: - "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 1.30.1(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 1.30.1(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) "@opentelemetry/semantic-conventions": 1.40.0 - "@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)": - dependencies: - "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 1.30.1(@opentelemetry/api@1.9.0) - "@opentelemetry/semantic-conventions": 1.28.0 - - "@opentelemetry/resources@2.0.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.9.0(@opentelemetry/api@1.9.0) "@opentelemetry/semantic-conventions": 1.40.0 - "@opentelemetry/resources@2.5.1(@opentelemetry/api@1.9.0)": + "@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.5.1(@opentelemetry/api@1.9.0) + "@opentelemetry/api-logs": 0.219.0 + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) "@opentelemetry/semantic-conventions": 1.40.0 - "@opentelemetry/sdk-logs@0.200.0(@opentelemetry/api@1.9.0)": - dependencies: - "@opentelemetry/api": 1.9.0 - "@opentelemetry/api-logs": 0.200.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) - - "@opentelemetry/sdk-metrics@2.0.0(@opentelemetry/api@1.9.0)": - dependencies: - "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) - - "@opentelemetry/sdk-metrics@2.5.1(@opentelemetry/api@1.9.0)": + "@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.5.1(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.5.1(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-node@0.200.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/sdk-node@0.219.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/api-logs": 0.200.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-logs-otlp-grpc": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-logs-otlp-http": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-logs-otlp-proto": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-metrics-otlp-grpc": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-metrics-otlp-http": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-metrics-otlp-proto": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-prometheus": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-trace-otlp-grpc": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-trace-otlp-http": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-trace-otlp-proto": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-zipkin": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/instrumentation": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/propagator-b3": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/propagator-jaeger": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-logs": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-metrics": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-trace-base": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-trace-node": 2.0.0(@opentelemetry/api@1.9.0) + "@opentelemetry/api-logs": 0.219.0 + "@opentelemetry/configuration": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/context-async-hooks": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-logs-otlp-grpc": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-logs-otlp-http": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-logs-otlp-proto": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-metrics-otlp-grpc": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-metrics-otlp-http": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-metrics-otlp-proto": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-prometheus": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-trace-otlp-grpc": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-trace-otlp-http": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-trace-otlp-proto": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/exporter-zipkin": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/instrumentation": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/otlp-grpc-exporter-base": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/propagator-b3": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/propagator-jaeger": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-logs": 0.219.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-metrics": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-trace-base": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-trace-node": 2.8.0(@opentelemetry/api@1.9.0) "@opentelemetry/semantic-conventions": 1.40.0 transitivePeerDependencies: - supports-color - "@opentelemetry/sdk-trace-base@2.0.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.0.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.8.0(@opentelemetry/api@1.9.0) "@opentelemetry/semantic-conventions": 1.40.0 - "@opentelemetry/sdk-trace-base@2.5.1(@opentelemetry/api@1.9.0)": + "@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/core": 2.5.1(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.5.1(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.9.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.9.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-trace": 2.9.0(@opentelemetry/api@1.9.0) "@opentelemetry/semantic-conventions": 1.40.0 - "@opentelemetry/sdk-trace-node@2.0.0(@opentelemetry/api@1.9.0)": + "@opentelemetry/sdk-trace-node@2.8.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/context-async-hooks": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/core": 2.0.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-trace-base": 2.0.0(@opentelemetry/api@1.9.0) + "@opentelemetry/context-async-hooks": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.8.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-trace-base": 2.8.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-trace-node@2.5.1(@opentelemetry/api@1.9.0)": + "@opentelemetry/sdk-trace-node@2.9.0(@opentelemetry/api@1.9.0)": dependencies: "@opentelemetry/api": 1.9.0 - "@opentelemetry/context-async-hooks": 2.5.1(@opentelemetry/api@1.9.0) - "@opentelemetry/core": 2.5.1(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-trace-base": 2.5.1(@opentelemetry/api@1.9.0) + "@opentelemetry/context-async-hooks": 2.9.0(@opentelemetry/api@1.9.0) + "@opentelemetry/core": 2.9.0(@opentelemetry/api@1.9.0) + "@opentelemetry/sdk-trace-base": 2.9.0(@opentelemetry/api@1.9.0) - "@opentelemetry/semantic-conventions@1.28.0": {} + "@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.0)": + dependencies: + "@opentelemetry/api": 1.9.0 + "@opentelemetry/core": 2.9.0(@opentelemetry/api@1.9.0) + "@opentelemetry/resources": 2.9.0(@opentelemetry/api@1.9.0) + "@opentelemetry/semantic-conventions": 1.40.0 "@opentelemetry/semantic-conventions@1.40.0": {} @@ -22932,24 +22709,21 @@ snapshots: "@protobufjs/base64@1.1.2": {} - "@protobufjs/codegen@2.0.4": {} + "@protobufjs/codegen@2.0.5": {} - "@protobufjs/eventemitter@1.1.0": {} + "@protobufjs/eventemitter@1.1.1": {} - "@protobufjs/fetch@1.1.0": + "@protobufjs/fetch@1.1.1": dependencies: "@protobufjs/aspromise": 1.1.2 - "@protobufjs/inquire": 1.1.0 "@protobufjs/float@1.0.2": {} - "@protobufjs/inquire@1.1.0": {} - "@protobufjs/path@1.1.2": {} "@protobufjs/pool@1.1.0": {} - "@protobufjs/utf8@1.1.0": {} + "@protobufjs/utf8@1.1.2": {} "@puppeteer/browsers@2.13.0": dependencies: @@ -24216,8 +23990,6 @@ snapshots: "@types/node": 25.3.0 "@types/send": 0.17.6 - "@types/shimmer@1.2.0": {} - "@types/sinonjs__fake-timers@8.1.1": {} "@types/sizzle@2.3.10": {} @@ -24249,56 +24021,33 @@ snapshots: transitivePeerDependencies: - supports-color - "@vitest/coverage-v8@4.0.7(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))": + "@vitest/coverage-v8@4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))": dependencies: "@bcoe/v8-coverage": 1.0.2 - "@vitest/utils": 4.0.7 - ast-v8-to-istanbul: 0.3.12 - debug: 4.4.3(supports-color@8.1.1) - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 - istanbul-reports: 3.2.0 - magicast: 0.3.5 - std-env: 3.10.0 - tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - transitivePeerDependencies: - - supports-color - - "@vitest/coverage-v8@4.0.7(vitest@4.0.7(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))": - dependencies: - "@bcoe/v8-coverage": 1.0.2 - "@vitest/utils": 4.0.7 + "@vitest/utils": 4.0.18 ast-v8-to-istanbul: 0.3.12 - debug: 4.4.3(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.2.0 - magicast: 0.3.5 + magicast: 0.5.4 + obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.7(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - transitivePeerDependencies: - - supports-color + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - "@vitest/coverage-v8@4.0.7(vitest@4.0.7(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))": + "@vitest/coverage-v8@4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))": dependencies: "@bcoe/v8-coverage": 1.0.2 - "@vitest/utils": 4.0.7 + "@vitest/utils": 4.0.18 ast-v8-to-istanbul: 0.3.12 - debug: 4.4.3(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.2.0 - magicast: 0.3.5 + magicast: 0.5.4 + obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.7(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - transitivePeerDependencies: - - supports-color + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) "@vitest/expect@2.0.5": dependencies: @@ -24324,34 +24073,17 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - "@vitest/expect@4.0.7": - dependencies: - "@standard-schema/spec": 1.1.0 - "@types/chai": 5.2.3 - "@vitest/spy": 4.0.7 - "@vitest/utils": 4.0.7 - chai: 6.2.2 - tinyrainbow: 3.0.3 - - "@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))": + "@vitest/mocker@4.0.18(vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))": dependencies: "@vitest/spy": 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - - "@vitest/mocker@4.0.7(vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))": - dependencies: - "@vitest/spy": 4.0.7 - estree-walker: 3.0.3 - magic-string: 0.30.21 optionalDependencies: vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - "@vitest/mocker@4.0.7(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))": + "@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))": dependencies: - "@vitest/spy": 4.0.7 + "@vitest/spy": 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: @@ -24373,32 +24105,17 @@ snapshots: dependencies: tinyrainbow: 3.0.3 - "@vitest/pretty-format@4.0.7": - dependencies: - tinyrainbow: 3.0.3 - "@vitest/runner@4.0.18": dependencies: "@vitest/utils": 4.0.18 pathe: 2.0.3 - "@vitest/runner@4.0.7": - dependencies: - "@vitest/utils": 4.0.7 - pathe: 2.0.3 - "@vitest/snapshot@4.0.18": dependencies: "@vitest/pretty-format": 4.0.18 magic-string: 0.30.21 pathe: 2.0.3 - "@vitest/snapshot@4.0.7": - dependencies: - "@vitest/pretty-format": 4.0.7 - magic-string: 0.30.21 - pathe: 2.0.3 - "@vitest/spy@2.0.5": dependencies: tinyspy: 3.0.2 @@ -24409,8 +24126,6 @@ snapshots: "@vitest/spy@4.0.18": {} - "@vitest/spy@4.0.7": {} - "@vitest/utils@2.0.5": dependencies: "@vitest/pretty-format": 2.0.5 @@ -24435,11 +24150,6 @@ snapshots: "@vitest/pretty-format": 4.0.18 tinyrainbow: 3.0.3 - "@vitest/utils@4.0.7": - dependencies: - "@vitest/pretty-format": 4.0.7 - tinyrainbow: 3.0.3 - "@volar/language-core@2.4.28": dependencies: "@volar/source-map": 2.4.28 @@ -24465,42 +24175,6 @@ snapshots: transitivePeerDependencies: - debug - "@vtex/diagnostics-nodejs@5.1.1(@opentelemetry/api@1.9.0)(@types/express@4.17.25)(express@4.22.1)": - dependencies: - "@aws-sdk/client-s3": 3.994.0 - "@grpc/grpc-js": 1.14.3 - "@opentelemetry/api": 1.9.0 - "@opentelemetry/api-logs": 0.200.0 - "@opentelemetry/baggage-span-processor": 0.4.2(@opentelemetry/api@1.9.0) - "@opentelemetry/context-async-hooks": 2.5.1(@opentelemetry/api@1.9.0) - "@opentelemetry/core": 2.5.1(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-logs-otlp-grpc": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-metrics-otlp-grpc": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/exporter-trace-otlp-grpc": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/instrumentation": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/instrumentation-express": 0.47.1(@opentelemetry/api@1.9.0) - "@opentelemetry/instrumentation-http": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/instrumentation-net": 0.44.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resource-detector-aws": 1.12.0(@opentelemetry/api@1.9.0) - "@opentelemetry/resources": 2.5.1(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-logs": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-metrics": 2.5.1(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-node": 0.200.0(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-trace-base": 2.5.1(@opentelemetry/api@1.9.0) - "@opentelemetry/sdk-trace-node": 2.5.1(@opentelemetry/api@1.9.0) - "@opentelemetry/semantic-conventions": 1.40.0 - "@vtex/diagnostics-semconv": 5.1.0 - tslib: 2.8.1 - uuid: 11.1.0 - optionalDependencies: - "@types/express": 4.17.25 - express: 4.22.1 - transitivePeerDependencies: - - aws-crt - - supports-color - - "@vtex/diagnostics-semconv@5.1.0": {} - "@vtex/faststore-sdk@0.1.1-canary.5": {} "@vtex/prettier-config@1.0.0(prettier@2.8.8)": @@ -24684,10 +24358,6 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 - acorn-import-attributes@1.9.5(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - acorn-import-phases@1.0.4(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -26300,6 +25970,8 @@ snapshots: es-module-lexer@2.0.0: {} + es-module-lexer@2.3.1: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -27422,18 +27094,10 @@ snapshots: import-from@4.0.0: {} - import-in-the-middle@1.15.0: + import-in-the-middle@3.3.2: dependencies: - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) - cjs-module-lexer: 1.4.3 - module-details-from-path: 1.0.4 - - import-in-the-middle@2.0.6: - dependencies: - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) cjs-module-lexer: 2.2.0 + es-module-lexer: 2.3.1 module-details-from-path: 1.0.4 import-lazy@4.0.0: {} @@ -27797,14 +27461,6 @@ snapshots: transitivePeerDependencies: - supports-color - istanbul-lib-source-maps@5.0.6: - dependencies: - "@jridgewell/trace-mapping": 0.3.31 - debug: 4.4.3(supports-color@8.1.1) - istanbul-lib-coverage: 3.2.2 - transitivePeerDependencies: - - supports-color - istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 @@ -28324,10 +27980,10 @@ snapshots: dependencies: "@jridgewell/sourcemap-codec": 1.5.5 - magicast@0.3.5: + magicast@0.5.4: dependencies: - "@babel/parser": 7.29.0 - "@babel/types": 7.29.0 + "@babel/parser": 7.29.8 + "@babel/types": 7.29.8 source-map-js: 1.2.1 make-dir@2.1.0: @@ -29562,18 +29218,17 @@ snapshots: proto-list@1.2.4: {} - protobufjs@7.5.4: + protobufjs@7.6.5: dependencies: "@protobufjs/aspromise": 1.1.2 "@protobufjs/base64": 1.1.2 - "@protobufjs/codegen": 2.0.4 - "@protobufjs/eventemitter": 1.1.0 - "@protobufjs/fetch": 1.1.0 + "@protobufjs/codegen": 2.0.5 + "@protobufjs/eventemitter": 1.1.1 + "@protobufjs/fetch": 1.1.1 "@protobufjs/float": 1.0.2 - "@protobufjs/inquire": 1.1.0 "@protobufjs/path": 1.1.2 "@protobufjs/pool": 1.1.0 - "@protobufjs/utf8": 1.1.0 + "@protobufjs/utf8": 1.1.2 "@types/node": 25.3.0 long: 5.3.2 @@ -29924,14 +29579,6 @@ snapshots: require-from-string@2.0.2: {} - require-in-the-middle@7.5.2: - dependencies: - debug: 4.4.3(supports-color@8.1.1) - module-details-from-path: 1.0.4 - resolve: 1.22.11 - transitivePeerDependencies: - - supports-color - require-in-the-middle@8.0.1: dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -30287,8 +29934,6 @@ snapshots: interpret: 1.4.0 rechoir: 0.6.2 - shimmer@1.2.1: {} - shx@0.3.4: dependencies: minimist: 1.2.8 @@ -31445,7 +31090,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitest-axe@0.1.0(vitest@4.0.7(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + vitest-axe@0.1.0(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): dependencies: aria-query: 5.3.2 axe-core: 4.11.1 @@ -31453,12 +31098,12 @@ snapshots: dom-accessibility-api: 0.5.16 lodash-es: 4.17.23 redent: 3.0.0 - vitest: 4.0.7(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: "@vitest/expect": 4.0.18 - "@vitest/mocker": 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + "@vitest/mocker": 4.0.18(vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) "@vitest/pretty-format": 4.0.18 "@vitest/runner": 4.0.18 "@vitest/snapshot": 4.0.18 @@ -31475,48 +31120,10 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - why-is-node-running: 2.3.0 - optionalDependencies: - "@opentelemetry/api": 1.9.0 - "@types/node": 25.3.0 - jsdom: 27.4.0 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml - - vitest@4.0.7(@types/node@20.19.13)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): - dependencies: - "@vitest/expect": 4.0.7 - "@vitest/mocker": 4.0.7(vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - "@vitest/pretty-format": 4.0.7 - "@vitest/runner": 4.0.7 - "@vitest/snapshot": 4.0.7 - "@vitest/spy": 4.0.7 - "@vitest/utils": 4.0.7 - debug: 4.4.3(supports-color@8.1.1) - es-module-lexer: 1.7.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: + "@opentelemetry/api": 1.9.0 "@types/node": 20.19.13 jsdom: 27.4.0 transitivePeerDependencies: @@ -31528,34 +31135,34 @@ snapshots: - sass-embedded - stylus - sugarss - - supports-color - terser - tsx - yaml - vitest@4.0.7(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.0)(jiti@2.6.1)(jsdom@27.4.0)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: - "@vitest/expect": 4.0.7 - "@vitest/mocker": 4.0.7(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - "@vitest/pretty-format": 4.0.7 - "@vitest/runner": 4.0.7 - "@vitest/snapshot": 4.0.7 - "@vitest/spy": 4.0.7 - "@vitest/utils": 4.0.7 - debug: 4.4.3(supports-color@8.1.1) + "@vitest/expect": 4.0.18 + "@vitest/mocker": 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + "@vitest/pretty-format": 4.0.18 + "@vitest/runner": 4.0.18 + "@vitest/snapshot": 4.0.18 + "@vitest/spy": 4.0.18 + "@vitest/utils": 4.0.18 es-module-lexer: 1.7.0 expect-type: 1.3.0 magic-string: 0.30.21 + obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.3 std-env: 3.10.0 tinybench: 2.9.0 - tinyexec: 0.3.2 + tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: + "@opentelemetry/api": 1.9.0 "@types/node": 25.3.0 jsdom: 27.4.0 transitivePeerDependencies: @@ -31567,7 +31174,6 @@ snapshots: - sass-embedded - stylus - sugarss - - supports-color - terser - tsx - yaml diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 92bed9e529..7c3ea56681 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -37,8 +37,8 @@ catalog: typescript: 5.9.3 vite: 7.1.5 vite-plugin-dts: 4.5.4 - vitest: 4.0.7 - '@vitest/coverage-v8': 4.0.7 + vitest: 4.0.18 + '@vitest/coverage-v8': 4.0.18 overrides: graphql: 16.11.0