diff --git a/CHANGELOG.md b/CHANGELOG.md index 145e0f1c6..ba31be812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +## [7.2.2] - 2025-09-26 +### Changed +- Adds integration with @vtex/diagnostics-semconv library to support VTEX semantic conventions for diagnostics-based logs + ## [7.2.1] - 2025-09-08 ### Fixed - Ensure `global.metrics` is initialized in both master and worker processes to prevent undefined errors in status tracking and metrics reporting diff --git a/__mocks__/@vtex/diagnostics-semconv.ts b/__mocks__/@vtex/diagnostics-semconv.ts new file mode 100644 index 000000000..29a85bedb --- /dev/null +++ b/__mocks__/@vtex/diagnostics-semconv.ts @@ -0,0 +1,14 @@ +// Mock para @vtex/diagnostics-semconv +const ATTR_VTEX_ACCOUNT_NAME = 'vtex.account.name' +const ATTR_VTEX_IO_WORKSPACE_NAME = 'vtex_io.workspace.name' +const ATTR_VTEX_IO_WORKSPACE_TYPE = 'vtex_io.workspace.type' +const ATTR_VTEX_IO_APP_ID = 'vtex_io.app.id' +const ATTR_VTEX_IO_APP_AUTHOR_TYPE = 'vtex_io.app.author-type' + +export { + ATTR_VTEX_ACCOUNT_NAME, + ATTR_VTEX_IO_WORKSPACE_NAME, + ATTR_VTEX_IO_WORKSPACE_TYPE, + ATTR_VTEX_IO_APP_ID, + ATTR_VTEX_IO_APP_AUTHOR_TYPE, +} diff --git a/jest.config.js b/jest.config.js index 9689a2108..c3cf78386 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,4 +5,7 @@ module.exports = { }, testRegex: '(.*(test|spec)).tsx?$', testEnvironment: 'node', + moduleNameMapper: { + '^@vtex/diagnostics-semconv$': '/__mocks__/@vtex/diagnostics-semconv.ts', + }, } diff --git a/package.json b/package.json index a4d9904da..63d975e6a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vtex/api", - "version": "7.2.1", + "version": "7.2.2", "description": "VTEX I/O API client", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -54,6 +54,7 @@ "@types/koa": "^2.11.0", "@types/koa-compose": "^3.2.3", "@vtex/diagnostics-nodejs": "0.1.0-io-beta.20", + "@vtex/diagnostics-semconv": "^1.1.2", "@vtex/node-error-report": "^0.0.3", "@wry/equality": "^0.1.9", "agentkeepalive": "^4.0.2", diff --git a/src/HttpClient/HttpClient.ts b/src/HttpClient/HttpClient.ts index fd4399826..6e5d19b45 100644 --- a/src/HttpClient/HttpClient.ts +++ b/src/HttpClient/HttpClient.ts @@ -3,16 +3,9 @@ import { createHash } from 'crypto' import { IncomingMessage } from 'http' import compose from 'koa-compose' import pLimit from 'p-limit' - import { - BINDING_HEADER, + HeaderKeys, BODY_HASH, - FORWARDED_HOST_HEADER, - LOCALE_HEADER, - PRODUCT_HEADER, - SEGMENT_HEADER, - SESSION_HEADER, - TENANT_HEADER, } from '../constants' import { Logger } from '../service/logger' import { IOContext } from '../service/worker/runtime/typings' @@ -89,14 +82,14 @@ export class HttpClient { ...defaultHeaders, 'Accept-Encoding': 'gzip', 'User-Agent': userAgent, - ...host ? { [FORWARDED_HOST_HEADER]: host } : null, - ...tenant ? { [TENANT_HEADER]: formatTenantHeaderValue(tenant) } : null, - ...binding ? { [BINDING_HEADER]: formatBindingHeaderValue(binding) } : null, - ...locale ? { [LOCALE_HEADER]: locale } : null, - ...operationId ? { 'x-vtex-operation-id': operationId } : null, - ...product ? { [PRODUCT_HEADER]: product } : null, - ...segmentToken ? { [SEGMENT_HEADER]: segmentToken } : null, - ...sessionToken ? { [SESSION_HEADER]: sessionToken } : null, + ...host ? { [HeaderKeys.FORWARDED_HOST]: host } : null, + ...tenant ? { [HeaderKeys.TENANT]: formatTenantHeaderValue(tenant) } : null, + ...binding ? { [HeaderKeys.BINDING]: formatBindingHeaderValue(binding) } : null, + ...locale ? { [HeaderKeys.LOCALE]: locale } : null, + ...operationId ? { [HeaderKeys.OPERATION_ID]: operationId } : null, + ...product ? { [HeaderKeys.PRODUCT]: product } : null, + ...segmentToken ? { [HeaderKeys.SEGMENT]: segmentToken } : null, + ...sessionToken ? { [HeaderKeys.SESSION]: sessionToken } : null, } if (authType && authToken) { @@ -139,16 +132,16 @@ export class HttpClient { return typeof v !== 'object' || v === null || Array.isArray(v) ? v : Object.fromEntries(Object.entries(v).sort(([ka], [kb]) => ka < kb ? -1 : ka > kb ? 1 : 0)) - } - catch(error) { + } + catch(error) { // I don't believe this will ever happen, but just in case // Also, I didn't include error as I am unsure if it would have sensitive information this.logger.warn({message: 'Error while sorting object for cache key'}) return v } } - - + + const bodyHash = createHash('md5').update(JSON.stringify(data, deterministicReplacer)).digest('hex') const cacheableConfig = this.getConfig(url, { ...config, diff --git a/src/HttpClient/middlewares/cache.ts b/src/HttpClient/middlewares/cache.ts index 8134d15b5..673831b54 100644 --- a/src/HttpClient/middlewares/cache.ts +++ b/src/HttpClient/middlewares/cache.ts @@ -2,7 +2,7 @@ import { AxiosRequestConfig, AxiosResponse } from 'axios' import { Span } from 'opentracing' import { CacheLayer } from '../../caches/CacheLayer' -import { LOCALE_HEADER, SEGMENT_HEADER, SESSION_HEADER } from '../../constants' +import { HeaderKeys } from '../../constants' import { IOContext } from '../../service/worker/runtime/typings' import { ErrorReport } from '../../tracing' import { HttpLogEvents } from '../../tracing/LogEvents' @@ -15,7 +15,7 @@ const cacheableStatusCodes = [200, 203, 204, 206, 300, 301, 404, 405, 410, 414, export const cacheKey = (config: AxiosRequestConfig) => { const {baseURL = '', url = '', params, headers} = config - const locale = headers?.[LOCALE_HEADER] + const locale = headers?.[HeaderKeys.LOCALE] const encodedBaseURL = baseURL.replace(/\//g, '\\') const encodedURL = url.replace(/\//g, '\\') @@ -97,7 +97,7 @@ export const cacheMiddleware = ({ type, storage, asyncSet }: CacheOptions) => { const { rootSpan: span, tracer, logger } = ctx.tracing ?? {} const key = cacheKey(ctx.config) - const segmentToken = ctx.config.headers?.[SEGMENT_HEADER] + const segmentToken = ctx.config.headers?.[HeaderKeys.SEGMENT] const keyWithSegment = key + segmentToken span?.log({ @@ -204,11 +204,11 @@ export const cacheMiddleware = ({ type, storage, asyncSet }: CacheOptions) => { } const shouldCache = maxAge || etag - const varySession = ctx.response.headers.vary && ctx.response.headers.vary.includes(SESSION_HEADER) + const varySession = ctx.response.headers.vary && ctx.response.headers.vary.includes(HeaderKeys.SESSION) if (shouldCache && !varySession) { const {responseType, responseEncoding: configResponseEncoding} = ctx.config const currentAge = revalidated ? 0 : age - const varySegment = ctx.response.headers.vary && ctx.response.headers.vary.includes(SEGMENT_HEADER) + const varySegment = ctx.response.headers.vary && ctx.response.headers.vary.includes(HeaderKeys.SEGMENT) const setKey = varySegment ? keyWithSegment : key const responseEncoding = configResponseEncoding || (responseType === 'arraybuffer' ? 'base64' : undefined) const cacheableData = type === CacheType.Disk && responseType === 'arraybuffer' diff --git a/src/HttpClient/middlewares/request/setupAxios/__tests__/axiosTracingTestSuite.ts b/src/HttpClient/middlewares/request/setupAxios/__tests__/axiosTracingTestSuite.ts index 3caff4f58..8d08411cd 100644 --- a/src/HttpClient/middlewares/request/setupAxios/__tests__/axiosTracingTestSuite.ts +++ b/src/HttpClient/middlewares/request/setupAxios/__tests__/axiosTracingTestSuite.ts @@ -1,7 +1,7 @@ import { MockSpanContext } from '@tiagonapoli/opentracing-alternate-mock' import { AxiosError, AxiosInstance } from 'axios' import { REFERENCE_CHILD_OF, REFERENCE_FOLLOWS_FROM } from 'opentracing' -import { ROUTER_CACHE_HEADER } from '../../../../../constants' +import { HeaderKeys } from '../../../../../constants' import { SpanReferenceTypes } from '../../../../../tracing' import { ErrorReportLogFields } from '../../../../../tracing/LogFields' import { CustomHttpTags, OpentracingTags } from '../../../../../tracing/Tags' @@ -141,7 +141,7 @@ export const registerSharedTestSuite = (testSuiteConfig: TestSuiteConfig) => { if (testSuiteConfig.testServer) { it(`Properly assigns router cache tag when it's present`, async () => { - testSuiteConfig.testServer!.mockResponseHeaders({ [ROUTER_CACHE_HEADER]: 'MISS' }) + testSuiteConfig.testServer!.mockResponseHeaders({ [HeaderKeys.ROUTER_CACHE]: 'MISS' }) const { allRequestSpans } = await TracedTestRequest.doRequest(http, testSuiteConfig.requestsConfig) allRequestSpans.forEach((requestSpan) => { expect(requestSpan.tags()[CustomHttpTags.HTTP_ROUTER_CACHE_RESULT]).toEqual('MISS') diff --git a/src/HttpClient/middlewares/request/setupAxios/interceptors/tracing/spanSetup.ts b/src/HttpClient/middlewares/request/setupAxios/interceptors/tracing/spanSetup.ts index d9ead2892..fc1996602 100644 --- a/src/HttpClient/middlewares/request/setupAxios/interceptors/tracing/spanSetup.ts +++ b/src/HttpClient/middlewares/request/setupAxios/interceptors/tracing/spanSetup.ts @@ -1,7 +1,7 @@ import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' import buildFullPath from '../../../../../../utils/buildFullPath' import { Span } from 'opentracing' -import { ROUTER_CACHE_HEADER } from '../../../../../../constants' +import { HeaderKeys } from '../../../../../../constants' import { CustomHttpTags, OpentracingTags } from '../../../../../../tracing/Tags' import { cloneAndSanitizeHeaders } from '../../../../../../tracing/utils' @@ -24,7 +24,8 @@ export const injectResponseInfoOnSpan = (span: Span | undefined, response: Axios span?.log({ 'response-headers': cloneAndSanitizeHeaders(response.headers) }) span?.setTag(OpentracingTags.HTTP_STATUS_CODE, response.status) - if (response.headers[ROUTER_CACHE_HEADER]) { - span?.setTag(CustomHttpTags.HTTP_ROUTER_CACHE_RESULT, response.headers[ROUTER_CACHE_HEADER]) + + if (response.headers[HeaderKeys.ROUTER_CACHE]) { + span?.setTag(CustomHttpTags.HTTP_ROUTER_CACHE_RESULT, response.headers[HeaderKeys.ROUTER_CACHE]) } } diff --git a/src/clients/janus/Segment.ts b/src/clients/janus/Segment.ts index 62f8a6126..6b27b4594 100644 --- a/src/clients/janus/Segment.ts +++ b/src/clients/janus/Segment.ts @@ -1,7 +1,6 @@ import parseCookie from 'cookie' import { prop } from 'ramda' - -import { PRODUCT_HEADER } from '../../constants' +import { HeaderKeys } from '../../constants' import { inflightUrlWithQuery, RequestTracingConfig } from '../../HttpClient' import { JanusClient } from './JanusClient' @@ -87,7 +86,7 @@ export class Segment extends JanusClient { forceMaxAge: SEGMENT_MAX_AGE_S, headers: { 'Content-Type': 'application/json', - [PRODUCT_HEADER]: product || '', + [HeaderKeys.PRODUCT]: product || '', }, inflightKey: inflightUrlWithQuery, metric, diff --git a/src/constants.test.ts b/src/constants.test.ts new file mode 100644 index 000000000..522acfb10 --- /dev/null +++ b/src/constants.test.ts @@ -0,0 +1,430 @@ +import { + NODE_VTEX_API_VERSION, + DEFAULT_WORKSPACE, + IS_IO, + PID, + HeaderKeys, + AttributeKeys, + BODY_HASH, + UP_SIGNAL, + MAX_AGE, + HTTP_SERVER_PORT, + MAX_WORKERS, + LINKED, + REGION, + PUBLIC_ENDPOINT, + APP, + NODE_ENV, + ACCOUNT, + WORKSPACE, + PRODUCTION, + INSPECT_DEBUGGER_PORT, + cancellableMethods, + LOG_CLIENT_INIT_TIMEOUT_MS +} from './constants' + +describe('constants', () => { + describe('Basic constants', () => { + test('NODE_VTEX_API_VERSION should match package.json version', () => { + const pkg = require('../package.json') + expect(NODE_VTEX_API_VERSION).toBe(pkg.version) + expect(typeof NODE_VTEX_API_VERSION).toBe('string') + expect(NODE_VTEX_API_VERSION.length).toBeGreaterThan(0) + }) + + test('DEFAULT_WORKSPACE should be a non-empty string', () => { + expect(typeof DEFAULT_WORKSPACE).toBe('string') + expect(DEFAULT_WORKSPACE.length).toBeGreaterThan(0) + }) + + test('IS_IO should reflect VTEX_IO environment variable', () => { + expect(IS_IO).toBe(process.env.VTEX_IO) + }) + + test('PID should be the current process ID', () => { + expect(PID).toBe(process.pid) + expect(typeof PID).toBe('number') + expect(PID).toBeGreaterThan(0) + }) + }) + + describe('HeaderKeys', () => { + test('should be an object with string properties', () => { + expect(typeof HeaderKeys).toBe('object') + expect(HeaderKeys).not.toBeNull() + }) + + test('all header keys should be uppercase with underscores', () => { + Object.keys(HeaderKeys).forEach(key => { + expect(key).toMatch(/^[A-Z_]+$/) + }) + }) + + test('VTEX headers should follow x-vtex- or x- naming pattern', () => { + const vtexSpecificHeaders = Object.entries(HeaderKeys).filter(([key]) => + key.includes('VTEX') || + key === 'ACCOUNT' || + key === 'WORKSPACE' || + key === 'OPERATION_ID' || + key === 'SEGMENT' + ) + + vtexSpecificHeaders.forEach(([, value]) => { + expect(value).toMatch(/^x-/) + }) + }) + + test('should not contain empty or invalid header values', () => { + Object.values(HeaderKeys).forEach(value => { + expect(value).not.toContain(' ') + expect(value).not.toMatch(/[A-Z]/) + expect(value).not.toContain('\n') + expect(value).not.toContain('\r') + }) + }) + }) + + describe('AttributeKeys', () => { + test('should be an object with string properties', () => { + expect(typeof AttributeKeys).toBe('object') + expect(AttributeKeys).not.toBeNull() + }) + + test('should contain VTEX semantic attributes', () => { + expect(AttributeKeys).toHaveProperty('VTEX_ACCOUNT_NAME') + + expect(typeof AttributeKeys.VTEX_ACCOUNT_NAME).toBe('string') + }) + + test('should contain VTEX IO semantic attributes', () => { + expect(AttributeKeys).toHaveProperty('VTEX_IO_WORKSPACE_NAME') + expect(AttributeKeys).toHaveProperty('VTEX_IO_WORKSPACE_TYPE') + expect(AttributeKeys).toHaveProperty('VTEX_IO_APP_ID') + expect(AttributeKeys).toHaveProperty('VTEX_IO_APP_AUTHOR_TYPE') + + expect(typeof AttributeKeys.VTEX_IO_WORKSPACE_NAME).toBe('string') + expect(typeof AttributeKeys.VTEX_IO_WORKSPACE_TYPE).toBe('string') + expect(typeof AttributeKeys.VTEX_IO_APP_ID).toBe('string') + expect(typeof AttributeKeys.VTEX_IO_APP_AUTHOR_TYPE).toBe('string') + }) + + test('should have non-empty string values', () => { + Object.values(AttributeKeys).forEach((value: any) => { + expect(typeof value).toBe('string') + expect(value.length).toBeGreaterThan(0) + }) + }) + + test('attribute names should follow naming convention', () => { + Object.keys(AttributeKeys).forEach(key => { + expect(key).toMatch(/^VTEX(_IO)?_[A-Z_]+$/) + }) + }) + + test('should import from external module without errors', () => { + // Test that the AttributeKeys structure exists and is properly imported + expect(AttributeKeys).toBeDefined() + Object.values(AttributeKeys).forEach(value => { + expect(value).toBeTruthy() + }) + }) + }) + + describe('Cache constants', () => { + test('MAX_AGE should be an object with numeric properties', () => { + expect(typeof MAX_AGE).toBe('object') + expect(MAX_AGE).not.toBeNull() + }) + + test('MAX_AGE should contain LONG, MEDIUM, SHORT properties', () => { + expect(MAX_AGE).toHaveProperty('LONG') + expect(MAX_AGE).toHaveProperty('MEDIUM') + expect(MAX_AGE).toHaveProperty('SHORT') + + expect(typeof MAX_AGE.LONG).toBe('number') + expect(typeof MAX_AGE.MEDIUM).toBe('number') + expect(typeof MAX_AGE.SHORT).toBe('number') + }) + + test('MAX_AGE values should be positive integers', () => { + Object.values(MAX_AGE).forEach(value => { + expect(value).toBeGreaterThan(0) + expect(Number.isInteger(value)).toBe(true) + }) + }) + + test('MAX_AGE values should be in logical order', () => { + expect(MAX_AGE.LONG).toBeGreaterThan(MAX_AGE.MEDIUM) + expect(MAX_AGE.MEDIUM).toBeGreaterThan(MAX_AGE.SHORT) + }) + }) + + describe('Server configuration', () => { + test('server ports should have specific expected values', () => { + expect(HTTP_SERVER_PORT).toBe(5050) + expect(INSPECT_DEBUGGER_PORT).toBe(5858) + + // Ensure they are numbers + expect(typeof HTTP_SERVER_PORT).toBe('number') + expect(typeof INSPECT_DEBUGGER_PORT).toBe('number') + }) + + test('MAX_WORKERS should be a positive integer', () => { + expect(typeof MAX_WORKERS).toBe('number') + expect(MAX_WORKERS).toBeGreaterThan(0) + expect(Number.isInteger(MAX_WORKERS)).toBe(true) + }) + + test('LOG_CLIENT_INIT_TIMEOUT_MS should be a positive number', () => { + expect(typeof LOG_CLIENT_INIT_TIMEOUT_MS).toBe('number') + expect(LOG_CLIENT_INIT_TIMEOUT_MS).toBeGreaterThan(0) + }) + }) + + describe('Environment-based constants', () => { + test('boolean environment constants should have correct types', () => { + expect(typeof LINKED).toBe('boolean') + expect(typeof PRODUCTION).toBe('boolean') + }) + + test('string environment constants should match their env vars', () => { + expect(REGION).toBe(process.env.VTEX_REGION as string) + expect(NODE_ENV).toBe(process.env.NODE_ENV as string) + expect(ACCOUNT).toBe(process.env.VTEX_ACCOUNT as string) + expect(WORKSPACE).toBe(process.env.VTEX_WORKSPACE as string) + }) + + test('PUBLIC_ENDPOINT should have a fallback value', () => { + expect(typeof PUBLIC_ENDPOINT).toBe('string') + expect(PUBLIC_ENDPOINT.length).toBeGreaterThan(0) + }) + + test('LINKED should reflect boolean conversion of VTEX_APP_LINK', () => { + expect(LINKED).toBe(!!process.env.VTEX_APP_LINK) + }) + + test('PRODUCTION should reflect string comparison', () => { + expect(PRODUCTION).toBe(process.env.VTEX_PRODUCTION === 'true') + }) + }) + + describe('APP object', () => { + test('should be an object with required properties', () => { + expect(typeof APP).toBe('object') + expect(APP).not.toBeNull() + + const requiredProperties = ['ID', 'MAJOR', 'NAME', 'VENDOR', 'VERSION', 'IS_THIRD_PARTY'] + requiredProperties.forEach(prop => { + expect(APP).toHaveProperty(prop) + }) + }) + + test('string properties should match environment variables', () => { + expect(APP.ID).toBe(process.env.VTEX_APP_ID as string) + expect(APP.NAME).toBe(process.env.VTEX_APP_NAME as string) + expect(APP.VENDOR).toBe(process.env.VTEX_APP_VENDOR as string) + expect(APP.VERSION).toBe(process.env.VTEX_APP_VERSION as string) + }) + + test('MAJOR should be extracted from VERSION', () => { + if (process.env.VTEX_APP_VERSION) { + expect(typeof APP.MAJOR).toBe('string') + } else { + expect(APP.MAJOR).toBe('') + } + }) + + test('IS_THIRD_PARTY should be a function', () => { + expect(typeof APP.IS_THIRD_PARTY).toBe('function') + }) + + test('IS_THIRD_PARTY should return boolean', () => { + const result = APP.IS_THIRD_PARTY() + expect(typeof result).toBe('boolean') + }) + + test('IS_THIRD_PARTY logic should work correctly', () => { + // Test the logic with different vendor values + const testCases = [ + { vendor: 'vtex', expected: false }, + { vendor: 'gocommerce', expected: false }, + { vendor: 'other', expected: true }, + { vendor: undefined, expected: true } + ] + + testCases.forEach(({ vendor, expected }) => { + const mockApp = { + VENDOR: vendor, + IS_THIRD_PARTY() { + return 'vtex' !== this.VENDOR && 'gocommerce' !== this.VENDOR + } + } + expect(mockApp.IS_THIRD_PARTY()).toBe(expected) + }) + }) + }) + + describe('HTTP methods', () => { + test('cancellableMethods should be a Set', () => { + expect(cancellableMethods).toBeInstanceOf(Set) + }) + + test('cancellableMethods should contain safe HTTP methods', () => { + expect(cancellableMethods.has('GET')).toBe(true) + expect(cancellableMethods.has('OPTIONS')).toBe(true) + expect(cancellableMethods.has('HEAD')).toBe(true) + }) + + test('cancellableMethods should not contain unsafe HTTP methods', () => { + const unsafeMethods = ['POST', 'PUT', 'DELETE', 'PATCH'] + unsafeMethods.forEach(method => { + expect(cancellableMethods.has(method)).toBe(false) + }) + }) + + test('cancellableMethods should have appropriate size', () => { + expect(cancellableMethods.size).toBeGreaterThan(0) + expect(cancellableMethods.size).toBeLessThan(10) // Reasonable upper bound + }) + + test('all methods in cancellableMethods should be strings', () => { + cancellableMethods.forEach((method: any) => { + expect(typeof method).toBe('string') + expect(method.length).toBeGreaterThan(0) + }) + }) + }) + + describe('Integration and dependencies', () => { + test('should import required modules without errors', () => { + expect(() => { + require('./utils/app') + }).not.toThrow() + }) + + test('package.json should be accessible and valid', () => { + const pkg = require('../package.json') + expect(pkg).toHaveProperty('name') + expect(pkg).toHaveProperty('version') + expect(typeof pkg.name).toBe('string') + expect(typeof pkg.version).toBe('string') + }) + + test('should handle external module imports gracefully', () => { + // Test that AttributeKeys exists even if external module has issues + expect(AttributeKeys).toBeDefined() + expect(typeof AttributeKeys).toBe('object') + }) + }) + + describe('Constants structure and exports', () => { + test('should export all required constants', () => { + // Test that all expected exports are defined using direct references + const constants = { + NODE_VTEX_API_VERSION, + DEFAULT_WORKSPACE, + IS_IO, + PID, + HeaderKeys, + AttributeKeys, + BODY_HASH, + UP_SIGNAL, + MAX_AGE, + HTTP_SERVER_PORT, + MAX_WORKERS, + LINKED, + REGION, + PUBLIC_ENDPOINT, + APP, + NODE_ENV, + ACCOUNT, + WORKSPACE, + PRODUCTION, + INSPECT_DEBUGGER_PORT, + cancellableMethods, + LOG_CLIENT_INIT_TIMEOUT_MS + } + + // Environment-based constants that can be undefined in test environment + const envBasedConstants = ['IS_IO', 'REGION', 'ACCOUNT', 'WORKSPACE', 'NODE_ENV'] + + Object.entries(constants).forEach(([name, value]) => { + if (envBasedConstants.includes(name)) { + // Environment-based constants can be undefined, but should not be null + expect(value).not.toBeNull() + } else { + // Other constants should be defined + expect(value).toBeDefined() + expect(value).not.toBeNull() + } + }) + }) + + test('constants should have consistent types', () => { + // Test type consistency + expect(typeof NODE_VTEX_API_VERSION).toBe('string') + expect(typeof DEFAULT_WORKSPACE).toBe('string') + expect(typeof PID).toBe('number') + expect(typeof HeaderKeys).toBe('object') + expect(typeof AttributeKeys).toBe('object') + expect(typeof BODY_HASH).toBe('string') + expect(typeof UP_SIGNAL).toBe('string') + expect(typeof MAX_AGE).toBe('object') + expect(typeof HTTP_SERVER_PORT).toBe('number') + expect(typeof MAX_WORKERS).toBe('number') + expect(typeof LINKED).toBe('boolean') + expect(typeof PUBLIC_ENDPOINT).toBe('string') + expect(typeof APP).toBe('object') + expect(typeof PRODUCTION).toBe('boolean') + expect(typeof INSPECT_DEBUGGER_PORT).toBe('number') + expect(cancellableMethods).toBeInstanceOf(Set) + expect(typeof LOG_CLIENT_INIT_TIMEOUT_MS).toBe('number') + }) + + test('should not have null or undefined critical constants', () => { + expect(NODE_VTEX_API_VERSION).not.toBeNull() + expect(DEFAULT_WORKSPACE).not.toBeNull() + expect(PID).not.toBeNull() + expect(HeaderKeys).not.toBeNull() + expect(AttributeKeys).not.toBeNull() + expect(BODY_HASH).not.toBeNull() + expect(UP_SIGNAL).not.toBeNull() + expect(MAX_AGE).not.toBeNull() + expect(APP).not.toBeNull() + expect(cancellableMethods).not.toBeNull() + }) + }) + + describe('Backward compatibility', () => { + test('should maintain critical header structure', () => { + // Test that critical headers exist without testing specific values + const criticalHeaders = ['ACCOUNT', 'WORKSPACE', 'OPERATION_ID', 'REQUEST_ID', 'TRACE_ID'] + + criticalHeaders.forEach(header => { + expect(HeaderKeys).toHaveProperty(header) + expect(typeof HeaderKeys[header as keyof typeof HeaderKeys]).toBe('string') + }) + }) + + test('should maintain AttributeKeys structure', () => { + const requiredAttributes = [ + 'VTEX_ACCOUNT_NAME', + 'VTEX_IO_WORKSPACE_NAME', 'VTEX_IO_WORKSPACE_TYPE', + 'VTEX_IO_APP_ID', 'VTEX_IO_APP_AUTHOR_TYPE' + ] + + requiredAttributes.forEach(attr => { + expect(AttributeKeys).toHaveProperty(attr) + expect(typeof AttributeKeys[attr as keyof typeof AttributeKeys]).toBe('string') + }) + }) + + test('should maintain APP object structure', () => { + const requiredProperties = ['ID', 'MAJOR', 'NAME', 'VENDOR', 'VERSION', 'IS_THIRD_PARTY'] + + requiredProperties.forEach(prop => { + expect(APP).toHaveProperty(prop) + }) + }) + }) +}) diff --git a/src/constants.ts b/src/constants.ts index 156860b6c..38f4c7630 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,12 @@ import { versionToMajor } from './utils/app' +import { + ATTR_VTEX_ACCOUNT_NAME, + ATTR_VTEX_IO_WORKSPACE_NAME, + ATTR_VTEX_IO_WORKSPACE_TYPE, + ATTR_VTEX_IO_APP_ID, + ATTR_VTEX_IO_APP_AUTHOR_TYPE +} from '@vtex/diagnostics-semconv' + // tslint:disable-next-line const pkg = require('../package.json') @@ -7,36 +15,56 @@ export const DEFAULT_WORKSPACE = 'master' export const IS_IO = process.env.VTEX_IO export const PID = process.pid -export const CACHE_CONTROL_HEADER = 'cache-control' -export const SEGMENT_HEADER = 'x-vtex-segment' -export const SESSION_HEADER = 'x-vtex-session' -export const PRODUCT_HEADER = 'x-vtex-product' -export const LOCALE_HEADER = 'x-vtex-locale' -export const FORWARDED_HOST_HEADER = 'x-forwarded-host' -export const TENANT_HEADER = 'x-vtex-tenant' -export const BINDING_HEADER = 'x-vtex-binding' -export const META_HEADER = 'x-vtex-meta' -export const META_HEADER_BUCKET = 'x-vtex-meta-bucket' -export const ETAG_HEADER = 'etag' -export const ACCOUNT_HEADER = 'x-vtex-account' -export const CREDENTIAL_HEADER = 'x-vtex-credential' -export const REQUEST_ID_HEADER = 'x-request-id' -export const ROUTER_CACHE_HEADER = 'x-router-cache' -export const OPERATION_ID_HEADER = 'x-vtex-operation-id' -export const PLATFORM_HEADER = 'x-vtex-platform' -export const WORKSPACE_IS_PRODUCTION_HEADER = 'x-vtex-workspace-is-production' -export const WORKSPACE_HEADER = 'x-vtex-workspace' -export const EVENT_KEY_HEADER = 'x-event-key' -export const EVENT_SENDER_HEADER = 'x-event-sender' -export const EVENT_SUBJECT_HEADER = 'x-event-subject' -export const EVENT_HANDLER_ID_HEADER = 'x-event-handler-id' -export const COLOSSUS_ROUTE_DECLARER_HEADER = 'x-colossus-route-declarer' -export const COLOSSUS_ROUTE_ID_HEADER = 'x-colossus-route-id' -export const COLOSSUS_PARAMS_HEADER = 'x-colossus-params' -export const TRACE_ID_HEADER = 'x-trace-id' -export const PROVIDER_HEADER = 'x-vtex-provider' - -export type VaryHeaders = typeof SEGMENT_HEADER | typeof SESSION_HEADER | typeof PRODUCT_HEADER | typeof LOCALE_HEADER +export const HeaderKeys = { + CACHE_CONTROL: 'cache-control', + SEGMENT: 'x-vtex-segment', + SESSION: 'x-vtex-session', + PRODUCT: 'x-vtex-product', + LOCALE: 'x-vtex-locale', + FORWARDED_HOST: 'x-forwarded-host', + FORWARDED_FOR: 'x-forwarded-for', + TENANT: 'x-vtex-tenant', + BINDING: 'x-vtex-binding', + META: 'x-vtex-meta', + META_BUCKET: 'x-vtex-meta-bucket', + ETAG: 'etag', + ACCOUNT: 'x-vtex-account', + CREDENTIAL: 'x-vtex-credential', + REQUEST_ID: 'x-request-id', + ROUTER_CACHE: 'x-router-cache', + OPERATION_ID: 'x-vtex-operation-id', + PLATFORM: 'x-vtex-platform', + WORKSPACE_IS_PRODUCTION: 'x-vtex-workspace-is-production', + WORKSPACE: 'x-vtex-workspace', + EVENT_KEY: 'x-event-key', + EVENT_SENDER: 'x-event-sender', + EVENT_SUBJECT: 'x-event-subject', + EVENT_HANDLER_ID: 'x-event-handler-id', + COLOSSUS_ROUTE_DECLARER: 'x-colossus-route-declarer', + COLOSSUS_ROUTE_ID: 'x-colossus-route-id', + COLOSSUS_PARAMS: 'x-colossus-params', + TRACE_ID: 'x-trace-id', + PROVIDER: 'x-vtex-provider', + USER_AGENT: 'user-agent', + VTEX_USER_AGENT: 'x-vtex-user-agent', + VTEX_IO_CALLER: 'x-vtex-io-caller', + VTEX_APP_SERVICE: 'x-vtex-app-service', + VTEX_APP_KEY: 'x-vtex-app-key', + VTEX_RETRY_COUNT: 'x-vtex-retry-count' +} + +export const AttributeKeys = { + // VTEX Semantic Attributes + VTEX_ACCOUNT_NAME: ATTR_VTEX_ACCOUNT_NAME, + + // VTEX IO Semantic Attributes + VTEX_IO_WORKSPACE_NAME: ATTR_VTEX_IO_WORKSPACE_NAME, + VTEX_IO_WORKSPACE_TYPE: ATTR_VTEX_IO_WORKSPACE_TYPE, + VTEX_IO_APP_ID: ATTR_VTEX_IO_APP_ID, + VTEX_IO_APP_AUTHOR_TYPE: ATTR_VTEX_IO_APP_AUTHOR_TYPE, +} + +export type VaryHeaders = typeof HeaderKeys.SEGMENT | typeof HeaderKeys.SESSION | typeof HeaderKeys.PRODUCT | typeof HeaderKeys.LOCALE export const BODY_HASH = '__graphqlBodyHash' diff --git a/src/service/logger/logger.ts b/src/service/logger/logger.ts index 3c4678364..1edbac51a 100644 --- a/src/service/logger/logger.ts +++ b/src/service/logger/logger.ts @@ -1,4 +1,4 @@ -import { APP, LOG_CLIENT_INIT_TIMEOUT_MS } from '../../constants' +import { APP, LOG_CLIENT_INIT_TIMEOUT_MS, AttributeKeys } from '../../constants' import { cleanError } from '../../utils/error' import { cleanLog } from '../../utils/log' import { Types } from '@vtex/diagnostics-nodejs'; @@ -82,13 +82,14 @@ export class Logger { const inflatedLog = { __VTEX_IO_LOG: true, level, - app, - account: this.account, - workspace: this.workspace, - production: this.production, - data, + [AttributeKeys.VTEX_IO_APP_ID]: app, + [AttributeKeys.VTEX_ACCOUNT_NAME]: this.account, + [AttributeKeys.VTEX_IO_WORKSPACE_NAME]: this.workspace, + [AttributeKeys.VTEX_IO_WORKSPACE_TYPE]: this.production ? 'production' : 'development', + [AttributeKeys.VTEX_IO_APP_AUTHOR_TYPE]: APP.IS_THIRD_PARTY() ? '3p' : '1p', operationId: this.operationId, requestId: this.requestId, + data, ... (this.tracingState?.isTraceSampled ? { traceId: this.tracingState.traceId } : null), } diff --git a/src/service/telemetry/client.ts b/src/service/telemetry/client.ts index 2a5312703..ad32fb3b2 100644 --- a/src/service/telemetry/client.ts +++ b/src/service/telemetry/client.ts @@ -6,7 +6,7 @@ import { Metrics, Traces, } from '@vtex/diagnostics-nodejs'; -import { APP, OTEL_EXPORTER_OTLP_ENDPOINT, DK_APP_ID, DIAGNOSTICS_TELEMETRY_ENABLED, WORKSPACE, PRODUCTION } from '../../constants'; +import { APP, OTEL_EXPORTER_OTLP_ENDPOINT, DK_APP_ID, DIAGNOSTICS_TELEMETRY_ENABLED, WORKSPACE, PRODUCTION, AttributeKeys } from '../../constants'; import { TelemetryClient } from '@vtex/diagnostics-nodejs/dist/telemetry'; import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; import { HostMetricsInstrumentation } from '../metrics/instruments/hostMetrics'; @@ -68,11 +68,11 @@ class TelemetryClientSingleton { // Use built-in no-op functionality when telemetry is disabled noop: !DIAGNOSTICS_TELEMETRY_ENABLED, additionalAttrs: { - 'app.id': APPLICATION_ID, + [AttributeKeys.VTEX_IO_APP_ID]: APPLICATION_ID, 'vendor': APP.VENDOR, 'version': APP.VERSION || '', - 'workspace': WORKSPACE, - 'production': PRODUCTION.toString(), + [AttributeKeys.VTEX_IO_WORKSPACE_NAME]: WORKSPACE, + [AttributeKeys.VTEX_IO_WORKSPACE_TYPE]: PRODUCTION.toString(), }, } ); diff --git a/src/service/tracing/tracingMiddlewares.ts b/src/service/tracing/tracingMiddlewares.ts index 0e3941397..945042b8a 100644 --- a/src/service/tracing/tracingMiddlewares.ts +++ b/src/service/tracing/tracingMiddlewares.ts @@ -1,6 +1,6 @@ import { FORMAT_HTTP_HEADERS, SpanContext, Tracer } from 'opentracing' import { finished as onStreamFinished } from 'stream' -import { ACCOUNT_HEADER, REQUEST_ID_HEADER, TRACE_ID_HEADER, WORKSPACE_HEADER } from '../../constants' +import { HeaderKeys } from '../../constants' import { ErrorReport, getTraceInfo } from '../../tracing' import { RuntimeLogEvents } from '../../tracing/LogEvents' import { RuntimeLogFields } from '../../tracing/LogFields' @@ -52,14 +52,14 @@ export const addTracingMiddleware = (tracer: Tracer) => { [OpentracingTags.HTTP_METHOD]: ctx.request.method, [OpentracingTags.HTTP_STATUS_CODE]: ctx.response.status, [CustomHttpTags.HTTP_PATH]: ctx.request.path, - [VTEXIncomingRequestTags.VTEX_REQUEST_ID]: ctx.get(REQUEST_ID_HEADER), - [VTEXIncomingRequestTags.VTEX_WORKSPACE]: ctx.get(WORKSPACE_HEADER), - [VTEXIncomingRequestTags.VTEX_ACCOUNT]: ctx.get(ACCOUNT_HEADER), + [VTEXIncomingRequestTags.VTEX_REQUEST_ID]: ctx.get(HeaderKeys.REQUEST_ID), + [VTEXIncomingRequestTags.VTEX_WORKSPACE]: ctx.get(HeaderKeys.WORKSPACE), + [VTEXIncomingRequestTags.VTEX_ACCOUNT]: ctx.get(HeaderKeys.ACCOUNT), }) currentSpan?.log(cloneAndSanitizeHeaders(ctx.request.headers, 'req.headers.')) currentSpan?.log(cloneAndSanitizeHeaders(ctx.response.headers, 'res.headers.')) - ctx.set(TRACE_ID_HEADER, traceInfo.traceId!) + ctx.set(HeaderKeys.TRACE_ID, traceInfo.traceId!) } const onResFinished = () => { diff --git a/src/service/worker/runtime/builtIn/middlewares.ts b/src/service/worker/runtime/builtIn/middlewares.ts index a9ce9b055..6c0561a54 100644 --- a/src/service/worker/runtime/builtIn/middlewares.ts +++ b/src/service/worker/runtime/builtIn/middlewares.ts @@ -1,6 +1,5 @@ import { collectDefaultMetrics, register } from 'prom-client' -import { COLOSSUS_ROUTE_ID_HEADER } from '../../../../constants' - +import { HeaderKeys } from '../../../../constants' import { MetricsLogger } from '../../../logger/metricsLogger' import { EventLoopLagMeasurer } from '../../../tracing/metrics/measurers/EventLoopLagMeasurer' import { ServiceContext } from '../typings' @@ -32,7 +31,7 @@ export const prometheusLoggerMiddleware = () => { return next() } - const routeId = ctx.get(COLOSSUS_ROUTE_ID_HEADER) + const routeId = ctx.get(HeaderKeys.COLOSSUS_ROUTE_ID) if (routeId) { return next() } diff --git a/src/service/worker/runtime/events/middlewares/context.ts b/src/service/worker/runtime/events/middlewares/context.ts index 773373c29..0b3d44f7f 100644 --- a/src/service/worker/runtime/events/middlewares/context.ts +++ b/src/service/worker/runtime/events/middlewares/context.ts @@ -1,9 +1,6 @@ import { IOClients } from '../../../../../clients/IOClients' import { - EVENT_HANDLER_ID_HEADER, - EVENT_KEY_HEADER, - EVENT_SENDER_HEADER, - EVENT_SUBJECT_HEADER, + HeaderKeys, } from '../../../../../constants' import { ParamsContext, RecorderState, ServiceContext } from '../../typings' import { prepareHandlerCtx } from '../../utils/context' @@ -13,12 +10,12 @@ export async function eventContextMiddleware | null) => { return async (ctx: ServiceContext, next: () => Promise) => { - const handlerId = ctx.get(EVENT_HANDLER_ID_HEADER) + const handlerId = ctx.get(HeaderKeys.EVENT_HANDLER_ID) if (!handlerId || !events) { return next() diff --git a/src/service/worker/runtime/graphql/middlewares/response.ts b/src/service/worker/runtime/graphql/middlewares/response.ts index cdfbad6b8..bd903994d 100644 --- a/src/service/worker/runtime/graphql/middlewares/response.ts +++ b/src/service/worker/runtime/graphql/middlewares/response.ts @@ -1,10 +1,5 @@ import { - CACHE_CONTROL_HEADER, - ETAG_HEADER, - FORWARDED_HOST_HEADER, - META_HEADER, - SEGMENT_HEADER, - SESSION_HEADER, + HeaderKeys, } from '../../../../../constants' import { Maybe } from '../../typings' import { Recorder } from '../../utils/recorder' @@ -12,14 +7,13 @@ import { GraphQLCacheControl, GraphQLServiceContext } from '../typings' import { cacheControlHTTP } from '../utils/cacheControl' function setVaryHeaders (ctx: GraphQLServiceContext, cacheControl: GraphQLCacheControl) { - ctx.vary(FORWARDED_HOST_HEADER) + ctx.vary(HeaderKeys.FORWARDED_HOST) if (cacheControl.scope === 'segment') { - ctx.vary(SEGMENT_HEADER) + ctx.vary(HeaderKeys.SEGMENT) } - if (cacheControl.scope === 'private' || ctx.query.scope === 'private') { - ctx.vary(SEGMENT_HEADER) - ctx.vary(SESSION_HEADER) + ctx.vary(HeaderKeys.SEGMENT) + ctx.vary(HeaderKeys.SESSION) } else if (ctx.vtex.sessionToken) { ctx.vtex.logger.warn({ message: 'GraphQL resolver receiving session token without private scope', @@ -29,9 +23,7 @@ function setVaryHeaders (ctx: GraphQLServiceContext, cacheControl: GraphQLCacheC } export async function response (ctx: GraphQLServiceContext, next: () => Promise) { - await next() - const { cacheControl, status, @@ -39,13 +31,12 @@ export async function response (ctx: GraphQLServiceContext, next: () => Promise< } = ctx.graphql const cacheControlHeader = cacheControlHTTP(ctx) - - ctx.set(CACHE_CONTROL_HEADER, cacheControlHeader) + ctx.set(HeaderKeys.CACHE_CONTROL, cacheControlHeader) if (status === 'error') { // Do not generate etag for errors - ctx.remove(META_HEADER) - ctx.remove(ETAG_HEADER) + ctx.remove(HeaderKeys.META) + ctx.remove(HeaderKeys.ETAG) ctx.vtex.recorder?.clear() } diff --git a/src/service/worker/runtime/graphql/middlewares/updateSchema.ts b/src/service/worker/runtime/graphql/middlewares/updateSchema.ts index 42c92cbde..5efd44b76 100644 --- a/src/service/worker/runtime/graphql/middlewares/updateSchema.ts +++ b/src/service/worker/runtime/graphql/middlewares/updateSchema.ts @@ -1,5 +1,5 @@ import { IOClients } from '../../../../../clients' -import { PROVIDER_HEADER } from '../../../../../constants' +import { HeaderKeys } from '../../../../../constants' import { majorEqualAndGreaterThan, parseAppId } from '../../../../../utils' import { GraphQLOptions, ParamsContext, RecorderState } from '../../typings' import { makeSchema } from '../schema/index' @@ -17,7 +17,7 @@ export const updateSchema = ) => { return async (ctx: ServiceContext, next: () => Promise) => { - const routeId = ctx.get(COLOSSUS_ROUTE_ID_HEADER) - + const routeId = ctx.get(HeaderKeys.COLOSSUS_ROUTE_ID) if (!routeId) { return next() } diff --git a/src/service/worker/runtime/utils/context.ts b/src/service/worker/runtime/utils/context.ts index c1e66c5f9..e4b64d4bb 100644 --- a/src/service/worker/runtime/utils/context.ts +++ b/src/service/worker/runtime/utils/context.ts @@ -1,22 +1,8 @@ import { Context } from 'koa' import uuid from 'uuid/v4' - import { - ACCOUNT_HEADER, - BINDING_HEADER, - CREDENTIAL_HEADER, - FORWARDED_HOST_HEADER, - LOCALE_HEADER, - OPERATION_ID_HEADER, - PLATFORM_HEADER, - PRODUCT_HEADER, + HeaderKeys, REGION, - REQUEST_ID_HEADER, - SEGMENT_HEADER, - SESSION_HEADER, - TENANT_HEADER, - WORKSPACE_HEADER, - WORKSPACE_IS_PRODUCTION_HEADER, } from '../../../../constants' import { UserLandTracer } from '../../../../tracing/UserLandTracer' import { parseTenantHeaderValue } from '../../../../utils/tenant' @@ -32,23 +18,23 @@ const getPlatform = (account: string): string => { export const prepareHandlerCtx = (header: Context['request']['header'], tracingContext?: TracingContext): HandlerContext => { const partialContext = { - account: header[ACCOUNT_HEADER], - authToken: header[CREDENTIAL_HEADER], - binding: header[BINDING_HEADER] ? parseBindingHeaderValue(header[BINDING_HEADER]) : undefined, - host: header[FORWARDED_HOST_HEADER], - locale: header[LOCALE_HEADER], - operationId: header[OPERATION_ID_HEADER] || uuid(), - platform: header[PLATFORM_HEADER] || getPlatform(header[ACCOUNT_HEADER]), - product: header[PRODUCT_HEADER], - production: header[WORKSPACE_IS_PRODUCTION_HEADER]?.toLowerCase() === 'true' || false, + account: header[HeaderKeys.ACCOUNT], + authToken: header[HeaderKeys.CREDENTIAL], + binding: header[HeaderKeys.BINDING] ? parseBindingHeaderValue(header[HeaderKeys.BINDING]) : undefined, + host: header[HeaderKeys.FORWARDED_HOST], + locale: header[HeaderKeys.LOCALE], + operationId: header[HeaderKeys.OPERATION_ID] || uuid(), + platform: header[HeaderKeys.PLATFORM] || getPlatform(header[HeaderKeys.ACCOUNT]), + product: header[HeaderKeys.PRODUCT], + production: header[HeaderKeys.WORKSPACE_IS_PRODUCTION]?.toLowerCase() === 'true' || false, region: REGION, - requestId: header[REQUEST_ID_HEADER], - segmentToken: header[SEGMENT_HEADER], - sessionToken: header[SESSION_HEADER], - tenant: header[TENANT_HEADER] ? parseTenantHeaderValue(header[TENANT_HEADER]) : undefined, + requestId: header[HeaderKeys.REQUEST_ID], + segmentToken: header[HeaderKeys.SEGMENT], + sessionToken: header[HeaderKeys.SESSION], + tenant: header[HeaderKeys.TENANT] ? parseTenantHeaderValue(header[HeaderKeys.TENANT]) : undefined, tracer: new UserLandTracer(tracingContext?.tracer!, tracingContext?.currentSpan), userAgent: process.env.VTEX_APP_ID || '', - workspace: header[WORKSPACE_HEADER], + workspace: header[HeaderKeys.WORKSPACE], } return { diff --git a/src/service/worker/runtime/utils/recorder.ts b/src/service/worker/runtime/utils/recorder.ts index ce5900e20..83be426aa 100644 --- a/src/service/worker/runtime/utils/recorder.ts +++ b/src/service/worker/runtime/utils/recorder.ts @@ -1,9 +1,8 @@ import { Context } from 'koa' import { trim } from 'ramda' +import { HeaderKeys } from './../../../../constants' -import { META_HEADER, META_HEADER_BUCKET } from './../../../../constants' - -const HEADERS = [META_HEADER, META_HEADER_BUCKET] +const HEADERS = [HeaderKeys.META, HeaderKeys.META_BUCKET] export class Recorder { // tslint:disable-next-line: variable-name diff --git a/yarn.lock b/yarn.lock index 2aaebf317..673ae7b6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1287,6 +1287,11 @@ resolved "https://registry.yarnpkg.com/@vtex/diagnostics-semconv/-/diagnostics-semconv-0.1.0-beta.11.tgz#2ddfff7dffdc1c052d23b335f914de91653d9659" integrity sha512-H3KM5fYAFmcxhlA4wT5iPgWJtgKsumFqGkkxjcA/BSwC5tgSWezN82sZDKvBsVo24EoZxVGgLlsjNw1tsp9U3Q== +"@vtex/diagnostics-semconv@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@vtex/diagnostics-semconv/-/diagnostics-semconv-1.1.2.tgz#ed58b4c0f403cf5d9ff5e3d487e959ff9c1802e2" + integrity sha512-CUz58FTeYHC6z5n0qJKcHesJK00ykwDAFKXUaBKjzI166lm/LqMkdPJA8KE2h4RWGDdSaPDIUDdkueSD76oUfw== + "@vtex/node-error-report@^0.0.3": version "0.0.3" resolved "https://registry.yarnpkg.com/@vtex/node-error-report/-/node-error-report-0.0.3.tgz#365a2652aeebbd6b51ddc5d64c3c0bc1a326dc71"