@opentelemetry/browser-instrumentation 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,7 +27,8 @@ export declare class FetchInstrumentation extends InstrumentationBase<FetchInstr
27
27
  */
28
28
  private _callRequestHook;
29
29
  /**
30
- * Add headers for the request and the given context if apply
30
+ * Add headers for the request and the given context if apply.
31
+ * Returns the options to send, a copy when the caller passed an init object.
31
32
  */
32
33
  private _addHeaders;
33
34
  }
@@ -126,7 +126,7 @@ var FetchInstrumentation = class extends InstrumentationBase {
126
126
  fetchStart: performance.now()
127
127
  });
128
128
  instrumentation._callRequestHook(createdSpan, options);
129
- instrumentation._addHeaders(options, url, fetchContext);
129
+ options = instrumentation._addHeaders(options, url, fetchContext);
130
130
  return original.apply(globalThis, options instanceof Request ? [options] : [url, options]).then(onSuccess.bind(globalThis, createdSpan), onError.bind(globalThis, createdSpan));
131
131
  });
132
132
  } catch (e) {
@@ -198,7 +198,8 @@ var FetchInstrumentation = class extends InstrumentationBase {
198
198
  }, true);
199
199
  }
200
200
  /**
201
- * Add headers for the request and the given context if apply
201
+ * Add headers for the request and the given context if apply.
202
+ * Returns the options to send, a copy when the caller passed an init object.
202
203
  */
203
204
  _addHeaders(options, url, ctx) {
204
205
  const urlsToPropagate = this.getConfig().propagateTraceHeaderCorsUrls;
@@ -208,13 +209,17 @@ var FetchInstrumentation = class extends InstrumentationBase {
208
209
  else {
209
210
  const headers = new Headers(options.headers);
210
211
  propagation.inject(ctx, headers, { set: (h, k, v) => h.set(k, typeof v === "string" ? v : String(v)) });
211
- options.headers = headers;
212
+ return {
213
+ ...options,
214
+ headers
215
+ };
212
216
  }
213
217
  } else {
214
218
  const headers = {};
215
219
  propagation.inject(ctx, headers);
216
220
  if (Object.keys(headers).length > 0) this._diag.debug("headers inject skipped due to CORS policy");
217
221
  }
222
+ return options;
218
223
  }
219
224
  };
220
225
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/fetch/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Attributes, Context, Span } from '@opentelemetry/api';\nimport {\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n} from '@opentelemetry/api';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport {\n ATTR_ERROR_TYPE,\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_REQUEST_METHOD_ORIGINAL,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_SERVER_ADDRESS,\n ATTR_SERVER_PORT,\n ATTR_URL_FULL,\n} from '@opentelemetry/semantic-conventions';\nimport { version } from '../../package.json' with { type: 'json' };\nimport { getNetworkContextRegistry } from '../utils/NetworkContextRegistry.ts';\nimport {\n getFetchBodyLength,\n normalizeHttpRequestMethod,\n} from '../utils/request.ts';\nimport { matchesUrl, parseUrl, serverPortFromUrl } from '../utils/url.ts';\nimport { ATTR_HTTP_REQUEST_BODY_SIZE } from './semconv.ts';\nimport type {\n FetchError,\n FetchInstrumentationConfig,\n FetchResponse,\n} from './types.ts';\n\nconst hasBrowserPerformanceAPI = typeof PerformanceObserver !== 'undefined';\n\nexport class FetchInstrumentation extends InstrumentationBase<FetchInstrumentationConfig> {\n // Note: Intentionally *not* using `_enabled` as the field name to avoid\n // any possible confusion with the `_enabled` field used on the *Node.js*\n // InstrumentationBase class.\n // Also not initializing the fields to `false` because the base class\n // constructor already call `enable` modifying their values and it will\n // set the instrumentations in a base state (enabled, patched but with flags set to false)\n declare private _isEnabled: boolean;\n declare private _isFetchPatched: boolean;\n\n // To keep track of the resources\n private _spanResources: Map<Span, PerformanceResourceTiming> = new Map();\n\n constructor(config: FetchInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/fetch', version, config);\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (!hasBrowserPerformanceAPI) {\n this._diag.warn(\n 'this instrumentation is intended for web usage only, it does not instrument server-side fetch()',\n );\n return;\n }\n\n if (this._isEnabled) {\n return;\n }\n\n if (this._isFetchPatched) {\n this._diag.debug('fetch constructor already patched');\n this._isEnabled = true;\n return;\n }\n\n try {\n // `_wrap` throws if a third-party script has locked globalThis.fetch via\n // Object.defineProperty(window, 'fetch', { writable: false, ... }).\n this._wrap(globalThis, 'fetch', this._patchConstructor());\n this._isFetchPatched = true;\n this._isEnabled = true;\n } catch (err) {\n this._diag.warn(\n 'Failed to patch globalThis.fetch; instrumentation will not be enabled. ' +\n 'Another script may have locked globalThis.fetch via Object.defineProperty.',\n err,\n );\n }\n }\n\n override disable(): void {\n if (!hasBrowserPerformanceAPI) {\n return;\n }\n\n if (!this._isEnabled) {\n return;\n }\n this._isEnabled = false;\n }\n\n /**\n * Patches the constructor of fetch\n */\n private _patchConstructor(): (original: typeof fetch) => typeof fetch {\n return (original) => {\n const instrumentation = this;\n\n return function patchConstructor(\n this: typeof globalThis,\n ...args: Parameters<typeof fetch>\n ): Promise<Response> {\n if (!instrumentation._isEnabled) {\n return original.apply(this, args);\n }\n\n // Reference to the Span in case there is an exception after creating it (wil not end)\n // It will be used to keep the Map only with the Spans that will be ended\n let trucatedSpan: Span | undefined;\n try {\n const instrConfig = instrumentation.getConfig();\n const url = parseUrl(\n args[0] instanceof Request ? args[0].url : String(args[0]),\n ).href;\n\n const shouldIgnoreUrl = matchesUrl(url, instrConfig.ignoreUrls);\n if (shouldIgnoreUrl) {\n return original.apply(this, args);\n }\n\n // Per the Fetch spec, when fetch() is called with a Request object\n // and a separate init object, the init properties override the\n // Request's properties. Merge them into a new Request so that\n // downstream consumers (hooks, header injection, the actual fetch\n // call) see the correct final values.\n // See: https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#parameters\n let options: Request | RequestInit;\n if (args[0] instanceof Request) {\n options = args[1] != null ? new Request(args[0], args[1]) : args[0];\n } else {\n options = args[1] || {};\n }\n\n const createdSpan = instrumentation._createSpan(url, options);\n trucatedSpan = createdSpan;\n if (instrConfig.measureRequestSize) {\n getFetchBodyLength(...args)\n .then((bodyLength) => {\n if (typeof bodyLength === 'number') {\n createdSpan.setAttribute(\n ATTR_HTTP_REQUEST_BODY_SIZE,\n bodyLength,\n );\n }\n })\n .catch((error) => {\n instrumentation._diag.warn('getFetchBodyLength', error);\n });\n }\n\n function endSpanOnError(span: Span, error: FetchError) {\n instrumentation._applyAttributesAfterFetch(span, options, error);\n instrumentation._endSpan(span, {\n status: error.status,\n error: error.name || error.message,\n aborted: error.name === 'AbortError',\n });\n }\n\n function endSpanOnSuccess(span: Span, response: Response) {\n instrumentation._applyAttributesAfterFetch(span, options, response);\n instrumentation._endSpan(span, response);\n }\n\n function onSuccess(span: Span, response: Response): Response {\n try {\n // Clone the response and eagerly consume the clone to detect\n // when the body transfer completes. The original response is\n // returned to the caller untouched so that it passes internal\n // brand-checks required by APIs such as\n // WebAssembly.compileStreaming.\n // It consumes the entire body even if the user cancels reading it\n // from the original response. But it does work with `AbortController.abort()`\n // because it aborts the underlying fetch cancelling the original and clone streams\n // ref: https://github.com/open-telemetry/opentelemetry-js/pull/6521\n const resClone = response.clone();\n const body = resClone.body;\n if (body) {\n const reader = body.getReader();\n\n const read = (): void => {\n reader.read().then(\n ({ done }) => {\n try {\n if (done) {\n endSpanOnSuccess(span, response);\n } else {\n read();\n }\n } catch (e) {\n instrumentation._diag.error(\n 'Failed to finalize span after body read',\n e,\n );\n instrumentation._endSpan(span, {\n status: response.status,\n });\n }\n },\n (error) => {\n try {\n endSpanOnError(span, {\n name: error?.name,\n message: error?.message,\n status: response.status,\n });\n } catch (e) {\n instrumentation._diag.error(\n 'Failed to end span after body read rejected',\n e,\n );\n instrumentation._endSpan(span, {\n status: response.status,\n });\n }\n },\n );\n };\n read();\n } else {\n // some older browsers don't have .body implemented\n endSpanOnSuccess(span, response);\n }\n } catch (error) {\n instrumentation._diag.error(\n 'Failed to read fetch response body',\n error,\n );\n instrumentation._endSpan(span, { status: response.status });\n }\n return response;\n }\n\n function onError(span: Span, error: FetchError): never {\n try {\n endSpanOnError(span, error);\n } catch (e: unknown) {\n // endSpanOnError failed — fall back to ending the span directly.\n // No `status`: no response was ever received for this request.\n instrumentation._diag.error(\n 'Failed to end span on fetch error',\n e,\n );\n instrumentation._endSpan(span, {});\n }\n throw error;\n }\n\n const fetchContext = trace.setSpan(context.active(), createdSpan);\n return context.with(fetchContext, () => {\n instrumentation._spanResources.set(createdSpan, {\n name: url,\n fetchStart: performance.now(),\n } as PerformanceResourceTiming);\n\n // Call request hook before injection so hooks cannot tamper with propagation headers.\n // Also, this means the hook will see `options.headers` in the same type as passed in,\n // rather than as a `Headers` instance set by `_addHeaders()`.\n instrumentation._callRequestHook(createdSpan, options);\n instrumentation._addHeaders(options, url, fetchContext);\n\n return original\n .apply(\n globalThis,\n options instanceof Request ? [options] : [url, options],\n )\n .then(\n onSuccess.bind(globalThis, createdSpan),\n onError.bind(globalThis, createdSpan),\n );\n });\n } catch (e: unknown) {\n instrumentation._diag.error('Failed to instrument fetch request', e);\n }\n // Remove the span if it was created and stored before the exception\n if (trucatedSpan) {\n instrumentation._spanResources.delete(trucatedSpan);\n }\n return original.apply(this, args);\n };\n };\n }\n\n /**\n * Creates a new span\n */\n private _createSpan(\n url: string,\n options: Partial<Request | RequestInit> = {},\n ): Span {\n const attributes = {} as Attributes;\n const parsedUrl = parseUrl(url);\n const origMethod = options.method;\n const normMethod = normalizeHttpRequestMethod(options.method || 'GET');\n const serverPort = serverPortFromUrl(parsedUrl);\n const name = normMethod;\n const { sanitizeUrl } = this.getConfig();\n\n attributes[ATTR_HTTP_REQUEST_METHOD] = normMethod;\n attributes[ATTR_URL_FULL] = sanitizeUrl ? sanitizeUrl(url) : url;\n attributes[ATTR_SERVER_ADDRESS] = parsedUrl.hostname;\n if (serverPort) {\n attributes[ATTR_SERVER_PORT] = serverPort;\n }\n if (origMethod && origMethod !== normMethod) {\n attributes[ATTR_HTTP_REQUEST_METHOD_ORIGINAL] = origMethod;\n }\n\n return this.tracer.startSpan(name, {\n kind: SpanKind.CLIENT,\n attributes,\n });\n }\n\n /**\n * Finish span, add attributes, etc.\n */\n private _endSpan(span: Span, response: FetchResponse) {\n if (response.status !== undefined) {\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);\n }\n\n // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#status\n // 1xx/2xx/3xx MUST be left unset UNLESS there was another error (e.g. a\n // network error receiving the response body)\n if (!response.aborted) {\n const isErrorStatus =\n response.status === undefined ||\n response.status >= 400 ||\n response.error !== undefined;\n if (isErrorStatus) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n const errorType =\n response.error ??\n (response.status !== undefined ? String(response.status) : undefined);\n if (typeof errorType === 'string') {\n span.setAttribute(ATTR_ERROR_TYPE, errorType);\n }\n }\n }\n\n span.end();\n\n // Register the resource context for other instrumentations\n const resource = this._spanResources.get(span);\n if (resource) {\n this._spanResources.delete(span);\n getNetworkContextRegistry().register(span, {\n key: resource.name,\n startPerfNow: resource.fetchStart,\n endPerfNow: performance.now(),\n });\n }\n }\n\n private _applyAttributesAfterFetch(\n span: Span,\n request: Request | RequestInit,\n result: Response | FetchError,\n ) {\n const applyCustomAttributesOnSpan =\n this.getConfig().applyCustomAttributesOnSpan;\n if (applyCustomAttributesOnSpan) {\n safeExecuteInTheMiddle(\n () => applyCustomAttributesOnSpan(span, request, result),\n (error) => {\n if (!error) {\n return;\n }\n this._diag.error('applyCustomAttributesOnSpan', error);\n },\n true,\n );\n }\n }\n\n /**\n * Calls the request hook if defined\n */\n private _callRequestHook(span: Span, request: Request | RequestInit) {\n const requestHook = this.getConfig().requestHook;\n\n if (requestHook) {\n safeExecuteInTheMiddle(\n () => requestHook(span, request),\n (error) => {\n if (error) {\n this._diag.error('requestHook', error);\n }\n },\n true,\n );\n }\n }\n\n /**\n * Add headers for the request and the given context if apply\n */\n private _addHeaders(\n options: Request | RequestInit,\n url: string,\n ctx: Context,\n ): void {\n // Propagate only if in request goes to same origin or is in the allow list\n const urlsToPropagate = this.getConfig().propagateTraceHeaderCorsUrls;\n const urlOrigin = parseUrl(url).origin;\n const sameOrigin = location.origin === urlOrigin;\n const shouldPropagate = sameOrigin || matchesUrl(url, urlsToPropagate);\n\n if (shouldPropagate) {\n if (options instanceof Request) {\n // This mutates `Request.headers` in-place, because it is read-only\n // (per https://developer.mozilla.org/en-US/docs/Web/API/Request/headers),\n // so we cannot (easily) replace it.\n propagation.inject(ctx, options.headers, {\n set: (h, k, v) => h.set(k, typeof v === 'string' ? v : String(v)),\n });\n } else {\n // Otherwise, create a new Headers to avoid mutating the caller's\n // possibly re-used headers.\n const headers = new Headers(options.headers);\n propagation.inject(ctx, headers, {\n set: (h, k, v) => h.set(k, typeof v === 'string' ? v : String(v)),\n });\n options.headers = headers;\n }\n } else {\n const headers: Partial<Record<string, unknown>> = {};\n propagation.inject(ctx, headers);\n if (Object.keys(headers).length > 0) {\n this._diag.debug('headers inject skipped due to CORS policy');\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"instrumentation.js","names":[],"sources":["../../src/fetch/instrumentation.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\nimport type { Attributes, Context, Span } from '@opentelemetry/api';\nimport {\n context,\n propagation,\n SpanKind,\n SpanStatusCode,\n trace,\n} from '@opentelemetry/api';\nimport {\n InstrumentationBase,\n safeExecuteInTheMiddle,\n} from '@opentelemetry/instrumentation';\nimport {\n ATTR_ERROR_TYPE,\n ATTR_HTTP_REQUEST_METHOD,\n ATTR_HTTP_REQUEST_METHOD_ORIGINAL,\n ATTR_HTTP_RESPONSE_STATUS_CODE,\n ATTR_SERVER_ADDRESS,\n ATTR_SERVER_PORT,\n ATTR_URL_FULL,\n} from '@opentelemetry/semantic-conventions';\nimport { version } from '../../package.json' with { type: 'json' };\nimport { getNetworkContextRegistry } from '../utils/NetworkContextRegistry.ts';\nimport {\n getFetchBodyLength,\n normalizeHttpRequestMethod,\n} from '../utils/request.ts';\nimport { matchesUrl, parseUrl, serverPortFromUrl } from '../utils/url.ts';\nimport { ATTR_HTTP_REQUEST_BODY_SIZE } from './semconv.ts';\nimport type {\n FetchError,\n FetchInstrumentationConfig,\n FetchResponse,\n} from './types.ts';\n\nconst hasBrowserPerformanceAPI = typeof PerformanceObserver !== 'undefined';\n\nexport class FetchInstrumentation extends InstrumentationBase<FetchInstrumentationConfig> {\n // Note: Intentionally *not* using `_enabled` as the field name to avoid\n // any possible confusion with the `_enabled` field used on the *Node.js*\n // InstrumentationBase class.\n // Also not initializing the fields to `false` because the base class\n // constructor already call `enable` modifying their values and it will\n // set the instrumentations in a base state (enabled, patched but with flags set to false)\n declare private _isEnabled: boolean;\n declare private _isFetchPatched: boolean;\n\n // To keep track of the resources\n private _spanResources: Map<Span, PerformanceResourceTiming> = new Map();\n\n constructor(config: FetchInstrumentationConfig = {}) {\n super('@opentelemetry/browser-instrumentation/fetch', version, config);\n }\n\n protected override init() {\n return [];\n }\n\n override enable(): void {\n if (!hasBrowserPerformanceAPI) {\n this._diag.warn(\n 'this instrumentation is intended for web usage only, it does not instrument server-side fetch()',\n );\n return;\n }\n\n if (this._isEnabled) {\n return;\n }\n\n if (this._isFetchPatched) {\n this._diag.debug('fetch constructor already patched');\n this._isEnabled = true;\n return;\n }\n\n try {\n // `_wrap` throws if a third-party script has locked globalThis.fetch via\n // Object.defineProperty(window, 'fetch', { writable: false, ... }).\n this._wrap(globalThis, 'fetch', this._patchConstructor());\n this._isFetchPatched = true;\n this._isEnabled = true;\n } catch (err) {\n this._diag.warn(\n 'Failed to patch globalThis.fetch; instrumentation will not be enabled. ' +\n 'Another script may have locked globalThis.fetch via Object.defineProperty.',\n err,\n );\n }\n }\n\n override disable(): void {\n if (!hasBrowserPerformanceAPI) {\n return;\n }\n\n if (!this._isEnabled) {\n return;\n }\n this._isEnabled = false;\n }\n\n /**\n * Patches the constructor of fetch\n */\n private _patchConstructor(): (original: typeof fetch) => typeof fetch {\n return (original) => {\n const instrumentation = this;\n\n return function patchConstructor(\n this: typeof globalThis,\n ...args: Parameters<typeof fetch>\n ): Promise<Response> {\n if (!instrumentation._isEnabled) {\n return original.apply(this, args);\n }\n\n // Reference to the Span in case there is an exception after creating it (wil not end)\n // It will be used to keep the Map only with the Spans that will be ended\n let trucatedSpan: Span | undefined;\n try {\n const instrConfig = instrumentation.getConfig();\n const url = parseUrl(\n args[0] instanceof Request ? args[0].url : String(args[0]),\n ).href;\n\n const shouldIgnoreUrl = matchesUrl(url, instrConfig.ignoreUrls);\n if (shouldIgnoreUrl) {\n return original.apply(this, args);\n }\n\n // Per the Fetch spec, when fetch() is called with a Request object\n // and a separate init object, the init properties override the\n // Request's properties. Merge them into a new Request so that\n // downstream consumers (hooks, header injection, the actual fetch\n // call) see the correct final values.\n // See: https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#parameters\n let options: Request | RequestInit;\n if (args[0] instanceof Request) {\n options = args[1] != null ? new Request(args[0], args[1]) : args[0];\n } else {\n options = args[1] || {};\n }\n\n const createdSpan = instrumentation._createSpan(url, options);\n trucatedSpan = createdSpan;\n if (instrConfig.measureRequestSize) {\n getFetchBodyLength(...args)\n .then((bodyLength) => {\n if (typeof bodyLength === 'number') {\n createdSpan.setAttribute(\n ATTR_HTTP_REQUEST_BODY_SIZE,\n bodyLength,\n );\n }\n })\n .catch((error) => {\n instrumentation._diag.warn('getFetchBodyLength', error);\n });\n }\n\n function endSpanOnError(span: Span, error: FetchError) {\n instrumentation._applyAttributesAfterFetch(span, options, error);\n instrumentation._endSpan(span, {\n status: error.status,\n error: error.name || error.message,\n aborted: error.name === 'AbortError',\n });\n }\n\n function endSpanOnSuccess(span: Span, response: Response) {\n instrumentation._applyAttributesAfterFetch(span, options, response);\n instrumentation._endSpan(span, response);\n }\n\n function onSuccess(span: Span, response: Response): Response {\n try {\n // Clone the response and eagerly consume the clone to detect\n // when the body transfer completes. The original response is\n // returned to the caller untouched so that it passes internal\n // brand-checks required by APIs such as\n // WebAssembly.compileStreaming.\n // It consumes the entire body even if the user cancels reading it\n // from the original response. But it does work with `AbortController.abort()`\n // because it aborts the underlying fetch cancelling the original and clone streams\n // ref: https://github.com/open-telemetry/opentelemetry-js/pull/6521\n const resClone = response.clone();\n const body = resClone.body;\n if (body) {\n const reader = body.getReader();\n\n const read = (): void => {\n reader.read().then(\n ({ done }) => {\n try {\n if (done) {\n endSpanOnSuccess(span, response);\n } else {\n read();\n }\n } catch (e) {\n instrumentation._diag.error(\n 'Failed to finalize span after body read',\n e,\n );\n instrumentation._endSpan(span, {\n status: response.status,\n });\n }\n },\n (error) => {\n try {\n endSpanOnError(span, {\n name: error?.name,\n message: error?.message,\n status: response.status,\n });\n } catch (e) {\n instrumentation._diag.error(\n 'Failed to end span after body read rejected',\n e,\n );\n instrumentation._endSpan(span, {\n status: response.status,\n });\n }\n },\n );\n };\n read();\n } else {\n // some older browsers don't have .body implemented\n endSpanOnSuccess(span, response);\n }\n } catch (error) {\n instrumentation._diag.error(\n 'Failed to read fetch response body',\n error,\n );\n instrumentation._endSpan(span, { status: response.status });\n }\n return response;\n }\n\n function onError(span: Span, error: FetchError): never {\n try {\n endSpanOnError(span, error);\n } catch (e: unknown) {\n // endSpanOnError failed — fall back to ending the span directly.\n // No `status`: no response was ever received for this request.\n instrumentation._diag.error(\n 'Failed to end span on fetch error',\n e,\n );\n instrumentation._endSpan(span, {});\n }\n throw error;\n }\n\n const fetchContext = trace.setSpan(context.active(), createdSpan);\n return context.with(fetchContext, () => {\n instrumentation._spanResources.set(createdSpan, {\n name: url,\n fetchStart: performance.now(),\n } as PerformanceResourceTiming);\n\n // Call request hook before injection so hooks cannot tamper with propagation headers.\n // Also, this means the hook will see `options.headers` in the same type as passed in,\n // rather than as a `Headers` instance set by `_addHeaders()`.\n instrumentation._callRequestHook(createdSpan, options);\n options = instrumentation._addHeaders(options, url, fetchContext);\n\n return original\n .apply(\n globalThis,\n options instanceof Request ? [options] : [url, options],\n )\n .then(\n onSuccess.bind(globalThis, createdSpan),\n onError.bind(globalThis, createdSpan),\n );\n });\n } catch (e: unknown) {\n instrumentation._diag.error('Failed to instrument fetch request', e);\n }\n // Remove the span if it was created and stored before the exception\n if (trucatedSpan) {\n instrumentation._spanResources.delete(trucatedSpan);\n }\n return original.apply(this, args);\n };\n };\n }\n\n /**\n * Creates a new span\n */\n private _createSpan(\n url: string,\n options: Partial<Request | RequestInit> = {},\n ): Span {\n const attributes = {} as Attributes;\n const parsedUrl = parseUrl(url);\n const origMethod = options.method;\n const normMethod = normalizeHttpRequestMethod(options.method || 'GET');\n const serverPort = serverPortFromUrl(parsedUrl);\n const name = normMethod;\n const { sanitizeUrl } = this.getConfig();\n\n attributes[ATTR_HTTP_REQUEST_METHOD] = normMethod;\n attributes[ATTR_URL_FULL] = sanitizeUrl ? sanitizeUrl(url) : url;\n attributes[ATTR_SERVER_ADDRESS] = parsedUrl.hostname;\n if (serverPort) {\n attributes[ATTR_SERVER_PORT] = serverPort;\n }\n if (origMethod && origMethod !== normMethod) {\n attributes[ATTR_HTTP_REQUEST_METHOD_ORIGINAL] = origMethod;\n }\n\n return this.tracer.startSpan(name, {\n kind: SpanKind.CLIENT,\n attributes,\n });\n }\n\n /**\n * Finish span, add attributes, etc.\n */\n private _endSpan(span: Span, response: FetchResponse) {\n if (response.status !== undefined) {\n span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);\n }\n\n // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#status\n // 1xx/2xx/3xx MUST be left unset UNLESS there was another error (e.g. a\n // network error receiving the response body)\n if (!response.aborted) {\n const isErrorStatus =\n response.status === undefined ||\n response.status >= 400 ||\n response.error !== undefined;\n if (isErrorStatus) {\n span.setStatus({ code: SpanStatusCode.ERROR });\n const errorType =\n response.error ??\n (response.status !== undefined ? String(response.status) : undefined);\n if (typeof errorType === 'string') {\n span.setAttribute(ATTR_ERROR_TYPE, errorType);\n }\n }\n }\n\n span.end();\n\n // Register the resource context for other instrumentations\n const resource = this._spanResources.get(span);\n if (resource) {\n this._spanResources.delete(span);\n getNetworkContextRegistry().register(span, {\n key: resource.name,\n startPerfNow: resource.fetchStart,\n endPerfNow: performance.now(),\n });\n }\n }\n\n private _applyAttributesAfterFetch(\n span: Span,\n request: Request | RequestInit,\n result: Response | FetchError,\n ) {\n const applyCustomAttributesOnSpan =\n this.getConfig().applyCustomAttributesOnSpan;\n if (applyCustomAttributesOnSpan) {\n safeExecuteInTheMiddle(\n () => applyCustomAttributesOnSpan(span, request, result),\n (error) => {\n if (!error) {\n return;\n }\n this._diag.error('applyCustomAttributesOnSpan', error);\n },\n true,\n );\n }\n }\n\n /**\n * Calls the request hook if defined\n */\n private _callRequestHook(span: Span, request: Request | RequestInit) {\n const requestHook = this.getConfig().requestHook;\n\n if (requestHook) {\n safeExecuteInTheMiddle(\n () => requestHook(span, request),\n (error) => {\n if (error) {\n this._diag.error('requestHook', error);\n }\n },\n true,\n );\n }\n }\n\n /**\n * Add headers for the request and the given context if apply.\n * Returns the options to send, a copy when the caller passed an init object.\n */\n private _addHeaders(\n options: Request | RequestInit,\n url: string,\n ctx: Context,\n ): Request | RequestInit {\n // Propagate only if in request goes to same origin or is in the allow list\n const urlsToPropagate = this.getConfig().propagateTraceHeaderCorsUrls;\n const urlOrigin = parseUrl(url).origin;\n const sameOrigin = location.origin === urlOrigin;\n const shouldPropagate = sameOrigin || matchesUrl(url, urlsToPropagate);\n\n if (shouldPropagate) {\n if (options instanceof Request) {\n // This mutates `Request.headers` in-place, because it is read-only\n // (per https://developer.mozilla.org/en-US/docs/Web/API/Request/headers),\n // so we cannot (easily) replace it.\n propagation.inject(ctx, options.headers, {\n set: (h, k, v) => h.set(k, typeof v === 'string' ? v : String(v)),\n });\n } else {\n // Otherwise, create a new Headers and a new init to avoid mutating\n // the caller's possibly re-used init and headers.\n const headers = new Headers(options.headers);\n propagation.inject(ctx, headers, {\n set: (h, k, v) => h.set(k, typeof v === 'string' ? v : String(v)),\n });\n return { ...options, headers };\n }\n } else {\n const headers: Partial<Record<string, unknown>> = {};\n propagation.inject(ctx, headers);\n if (Object.keys(headers).length > 0) {\n this._diag.debug('headers inject skipped due to CORS policy');\n }\n }\n return options;\n }\n}\n"],"mappings":"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"}
@@ -13,10 +13,9 @@ function isHashChange(fromUrl, toUrl) {
13
13
  const a = new URL(fromUrl, window.location.origin);
14
14
  const b = new URL(toUrl, window.location.origin);
15
15
  const sameBase = a.origin === b.origin && a.pathname === b.pathname && a.search === b.search;
16
- const fromHasHash = a.hash !== "";
17
16
  const toHasHash = b.hash !== "";
18
17
  const hashesAreDifferent = a.hash !== b.hash;
19
- return sameBase && hashesAreDifferent && (fromHasHash && toHasHash || !fromHasHash && toHasHash);
18
+ return sameBase && hashesAreDifferent && toHasHash;
20
19
  } catch {
21
20
  const fromBase = fromUrl.split("#")[0];
22
21
  const toBase = toUrl.split("#")[0];
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","names":[],"sources":["../../src/navigation/utils.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Determines if navigation between two URLs represents a hash change.\n * A hash change is true if the URLs are the same except for the hash part,\n * AND the hash is being added or changed (not removed).\n *\n * @param fromUrl - The source URL\n * @param toUrl - The destination URL\n * @returns true if this represents a hash change navigation\n */\nexport function isHashChange(fromUrl: string, toUrl: string): boolean {\n try {\n const a = new URL(fromUrl, window.location.origin);\n const b = new URL(toUrl, window.location.origin);\n const sameBase =\n a.origin === b.origin &&\n a.pathname === b.pathname &&\n a.search === b.search;\n const fromHasHash = a.hash !== '';\n const toHasHash = b.hash !== '';\n const hashesAreDifferent = a.hash !== b.hash;\n\n return (\n sameBase &&\n hashesAreDifferent &&\n ((fromHasHash && toHasHash) || (!fromHasHash && toHasHash))\n );\n } catch {\n const fromBase = fromUrl.split('#')[0];\n const toBase = toUrl.split('#')[0];\n const fromHash = fromUrl.split('#')[1] ?? '';\n const toHash = toUrl.split('#')[1] ?? '';\n\n const sameBase = fromBase === toBase;\n const hashesAreDifferent = fromHash !== toHash;\n const notRemovingHash = toHash !== '';\n\n return sameBase && hashesAreDifferent && notRemovingHash;\n }\n}\n"],"mappings":";;;;;;;;;;AAcA,SAAgB,aAAa,SAAiB,OAAwB;CACpE,IAAI;EACF,MAAM,IAAI,IAAI,IAAI,SAAS,OAAO,SAAS,MAAM;EACjD,MAAM,IAAI,IAAI,IAAI,OAAO,OAAO,SAAS,MAAM;EAC/C,MAAM,WACJ,EAAE,WAAW,EAAE,UACf,EAAE,aAAa,EAAE,YACjB,EAAE,WAAW,EAAE;EACjB,MAAM,cAAc,EAAE,SAAS;EAC/B,MAAM,YAAY,EAAE,SAAS;EAC7B,MAAM,qBAAqB,EAAE,SAAS,EAAE;EAExC,OACE,YACA,uBACE,eAAe,aAAe,CAAC,eAAe;CAEpD,QAAQ;EACN,MAAM,WAAW,QAAQ,MAAM,GAAG,CAAC,CAAC;EACpC,MAAM,SAAS,MAAM,MAAM,GAAG,CAAC,CAAC;EAChC,MAAM,WAAW,QAAQ,MAAM,GAAG,CAAC,CAAC,MAAM;EAC1C,MAAM,SAAS,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM;EAMtC,OAJiB,aAAa,UACH,aAAa,UAChB,WAAW;CAGrC;AACF"}
1
+ {"version":3,"file":"utils.js","names":[],"sources":["../../src/navigation/utils.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\n/**\n * Determines if navigation between two URLs represents a hash change.\n * A hash change is true if the URLs are the same except for the hash part,\n * AND the hash is being added or changed (not removed).\n *\n * @param fromUrl - The source URL\n * @param toUrl - The destination URL\n * @returns true if this represents a hash change navigation\n */\nexport function isHashChange(fromUrl: string, toUrl: string): boolean {\n try {\n const a = new URL(fromUrl, window.location.origin);\n const b = new URL(toUrl, window.location.origin);\n const sameBase =\n a.origin === b.origin &&\n a.pathname === b.pathname &&\n a.search === b.search;\n const toHasHash = b.hash !== '';\n const hashesAreDifferent = a.hash !== b.hash;\n\n return sameBase && hashesAreDifferent && toHasHash;\n } catch {\n const fromBase = fromUrl.split('#')[0];\n const toBase = toUrl.split('#')[0];\n const fromHash = fromUrl.split('#')[1] ?? '';\n const toHash = toUrl.split('#')[1] ?? '';\n\n const sameBase = fromBase === toBase;\n const hashesAreDifferent = fromHash !== toHash;\n const notRemovingHash = toHash !== '';\n\n return sameBase && hashesAreDifferent && notRemovingHash;\n }\n}\n"],"mappings":";;;;;;;;;;AAcA,SAAgB,aAAa,SAAiB,OAAwB;CACpE,IAAI;EACF,MAAM,IAAI,IAAI,IAAI,SAAS,OAAO,SAAS,MAAM;EACjD,MAAM,IAAI,IAAI,IAAI,OAAO,OAAO,SAAS,MAAM;EAC/C,MAAM,WACJ,EAAE,WAAW,EAAE,UACf,EAAE,aAAa,EAAE,YACjB,EAAE,WAAW,EAAE;EACjB,MAAM,YAAY,EAAE,SAAS;EAC7B,MAAM,qBAAqB,EAAE,SAAS,EAAE;EAExC,OAAO,YAAY,sBAAsB;CAC3C,QAAQ;EACN,MAAM,WAAW,QAAQ,MAAM,GAAG,CAAC,CAAC;EACpC,MAAM,SAAS,MAAM,MAAM,GAAG,CAAC,CAAC;EAChC,MAAM,WAAW,QAAQ,MAAM,GAAG,CAAC,CAAC,MAAM;EAC1C,MAAM,SAAS,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM;EAMtC,OAJiB,aAAa,UACH,aAAa,UAChB,WAAW;CAGrC;AACF"}
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.8.1";
2
+ var version = "0.8.2";
3
3
  //#endregion
4
4
  export { version };
5
5
 
@@ -31,9 +31,9 @@ const getNodeSelector = (element, useElementId = false) => {
31
31
  };
32
32
  const getNthChild = (element) => {
33
33
  if (!element.parentElement) return 0;
34
- const selector = getFullClassSelector(element);
35
- const siblings = Array.from(element.parentElement.children).filter((sibling) => getFullClassSelector(sibling) === selector);
36
- if (siblings.length > 1) return siblings.indexOf(element) + 1;
34
+ const classes = Array.from(element.classList);
35
+ const children = Array.from(element.parentElement.children);
36
+ if (children.some((sibling) => sibling !== element && sibling.localName === element.localName && classes.every((cls) => sibling.classList.contains(cls)))) return children.indexOf(element) + 1;
37
37
  return 0;
38
38
  };
39
39
  const getFullClassSelector = (element) => element.localName + (element.classList.length > 0 ? Array.from(element.classList).map((cls) => `.${CSS.escape(cls)}`).join("") : "");
@@ -1 +1 @@
1
- {"version":3,"file":"getElementCSSSelector.js","names":[],"sources":["../../src/utils/getElementCSSSelector.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\ntype GetElementCSSSelectorOptions = {\n /**\n * If true, the function will attempt to use element ID to create condensed CSS selector.\n */\n useIdForTargetElement?: boolean;\n /**\n * If true, the function will attempt to use element ID for all ancestor elements to create condensed CSS selector.\n */\n useIdForAncestors?: boolean;\n};\n\n/**\n * Generates the CSS selector of a given element in the DOM tree.\n *\n * @example #main > div:nth-child(2) > button.submit\n * @example #unique-id\n */\nexport const getElementCSSSelector = (\n element: Node,\n {\n useIdForTargetElement = false,\n useIdForAncestors = false,\n }: GetElementCSSSelectorOptions = {},\n): string => {\n // Handle document node\n if (element.nodeType === Node.DOCUMENT_NODE) {\n return '';\n }\n\n const htmlElement = element as HTMLElement;\n const nodeValue = getNodeSelector(\n htmlElement,\n useIdForTargetElement || useIdForAncestors,\n );\n\n // If optimized and found an ID selector, stop recursion early\n if (nodeValue.startsWith('#')) {\n return nodeValue;\n }\n\n const parent = htmlElement.parentElement;\n const parentSelector = parent\n ? getElementCSSSelector(parent, {\n useIdForAncestors,\n useIdForTargetElement: false,\n })\n : '';\n\n return parentSelector ? `${parentSelector} > ${nodeValue}` : nodeValue;\n};\n\nconst getNodeSelector = (element: Node, useElementId = false): string => {\n if (element.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const htmlElement = element as HTMLElement;\n const id = htmlElement.getAttribute('id');\n\n // Use ID if requested and it's unique\n if (useElementId && id) {\n // Check if ID is unique in the document\n const elementsWithSameId = htmlElement.ownerDocument.querySelectorAll(\n `#${CSS.escape(id)}`,\n );\n\n if (elementsWithSameId.length === 1) {\n return `#${CSS.escape(id)}`;\n }\n }\n\n let selector = getFullClassSelector(htmlElement);\n // Add nth-child if there are siblings with the same tag and classes\n const index = getNthChild(htmlElement);\n\n if (index > 0) {\n selector += `:nth-child(${index})`;\n }\n\n return selector;\n};\n\nconst getNthChild = (element: HTMLElement): number => {\n // parentElement is needed to access children\n if (!element.parentElement) {\n return 0;\n }\n\n const selector = getFullClassSelector(element);\n\n // Get all siblings that match the same selector\n const siblings = Array.from(element.parentElement.children).filter(\n (sibling) => getFullClassSelector(sibling) === selector,\n );\n\n // Only add nth-child if there are multiple matching siblings\n if (siblings.length > 1) {\n return siblings.indexOf(element) + 1;\n }\n\n return 0;\n};\n\nconst getFullClassSelector = (element: Element): string =>\n element.localName +\n (element.classList.length > 0\n ? Array.from(element.classList)\n .map((cls) => `.${CSS.escape(cls)}`)\n .join('')\n : '');\n"],"mappings":";;;;;;;AAsBA,MAAa,yBACX,SACA,EACE,wBAAwB,OACxB,oBAAoB,UACY,CAAC,MACxB;CAEX,IAAI,QAAQ,aAAa,KAAK,eAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,YAAY,gBAChB,aACA,yBAAyB,iBAC3B;CAGA,IAAI,UAAU,WAAW,GAAG,GAC1B,OAAO;CAGT,MAAM,SAAS,YAAY;CAC3B,MAAM,iBAAiB,SACnB,sBAAsB,QAAQ;EAC5B;EACA,uBAAuB;CACzB,CAAC,IACD;CAEJ,OAAO,iBAAiB,GAAG,eAAe,KAAK,cAAc;AAC/D;AAEA,MAAM,mBAAmB,SAAe,eAAe,UAAkB;CACvE,IAAI,QAAQ,aAAa,KAAK,cAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,KAAK,YAAY,aAAa,IAAI;CAGxC,IAAI,gBAAgB,IAES;MAAA,YAAY,cAAc,iBACnD,IAAI,IAAI,OAAO,EAAE,GAGE,CAAC,CAAC,WAAW,GAChC,OAAO,IAAI,IAAI,OAAO,EAAE;CAAA;CAI5B,IAAI,WAAW,qBAAqB,WAAW;CAE/C,MAAM,QAAQ,YAAY,WAAW;CAErC,IAAI,QAAQ,GACV,YAAY,cAAc,MAAM;CAGlC,OAAO;AACT;AAEA,MAAM,eAAe,YAAiC;CAEpD,IAAI,CAAC,QAAQ,eACX,OAAO;CAGT,MAAM,WAAW,qBAAqB,OAAO;CAG7C,MAAM,WAAW,MAAM,KAAK,QAAQ,cAAc,QAAQ,CAAC,CAAC,QACzD,YAAY,qBAAqB,OAAO,MAAM,QACjD;CAGA,IAAI,SAAS,SAAS,GACpB,OAAO,SAAS,QAAQ,OAAO,IAAI;CAGrC,OAAO;AACT;AAEA,MAAM,wBAAwB,YAC5B,QAAQ,aACP,QAAQ,UAAU,SAAS,IACxB,MAAM,KAAK,QAAQ,SAAS,CAAC,CAC1B,KAAK,QAAQ,IAAI,IAAI,OAAO,GAAG,GAAG,CAAC,CACnC,KAAK,EAAE,IACV"}
1
+ {"version":3,"file":"getElementCSSSelector.js","names":[],"sources":["../../src/utils/getElementCSSSelector.ts"],"sourcesContent":["/*\n * Copyright The OpenTelemetry Authors\n * SPDX-License-Identifier: Apache-2.0\n */\n\ntype GetElementCSSSelectorOptions = {\n /**\n * If true, the function will attempt to use element ID to create condensed CSS selector.\n */\n useIdForTargetElement?: boolean;\n /**\n * If true, the function will attempt to use element ID for all ancestor elements to create condensed CSS selector.\n */\n useIdForAncestors?: boolean;\n};\n\n/**\n * Generates the CSS selector of a given element in the DOM tree.\n *\n * @example #main > div:nth-child(2) > button.submit\n * @example #unique-id\n */\nexport const getElementCSSSelector = (\n element: Node,\n {\n useIdForTargetElement = false,\n useIdForAncestors = false,\n }: GetElementCSSSelectorOptions = {},\n): string => {\n // Handle document node\n if (element.nodeType === Node.DOCUMENT_NODE) {\n return '';\n }\n\n const htmlElement = element as HTMLElement;\n const nodeValue = getNodeSelector(\n htmlElement,\n useIdForTargetElement || useIdForAncestors,\n );\n\n // If optimized and found an ID selector, stop recursion early\n if (nodeValue.startsWith('#')) {\n return nodeValue;\n }\n\n const parent = htmlElement.parentElement;\n const parentSelector = parent\n ? getElementCSSSelector(parent, {\n useIdForAncestors,\n useIdForTargetElement: false,\n })\n : '';\n\n return parentSelector ? `${parentSelector} > ${nodeValue}` : nodeValue;\n};\n\nconst getNodeSelector = (element: Node, useElementId = false): string => {\n if (element.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const htmlElement = element as HTMLElement;\n const id = htmlElement.getAttribute('id');\n\n // Use ID if requested and it's unique\n if (useElementId && id) {\n // Check if ID is unique in the document\n const elementsWithSameId = htmlElement.ownerDocument.querySelectorAll(\n `#${CSS.escape(id)}`,\n );\n\n if (elementsWithSameId.length === 1) {\n return `#${CSS.escape(id)}`;\n }\n }\n\n let selector = getFullClassSelector(htmlElement);\n // Add nth-child if there are siblings with the same tag and classes\n const index = getNthChild(htmlElement);\n\n if (index > 0) {\n selector += `:nth-child(${index})`;\n }\n\n return selector;\n};\n\nconst getNthChild = (element: HTMLElement): number => {\n // parentElement is needed to access children\n if (!element.parentElement) {\n return 0;\n }\n\n const classes = Array.from(element.classList);\n const children = Array.from(element.parentElement.children);\n\n // The selector also matches any sibling with this tag and these classes\n const hasMatchingSibling = children.some(\n (sibling) =>\n sibling !== element &&\n sibling.localName === element.localName &&\n classes.every((cls) => sibling.classList.contains(cls)),\n );\n\n // nth-child counts every element sibling, not only the matching ones\n if (hasMatchingSibling) {\n return children.indexOf(element) + 1;\n }\n\n return 0;\n};\n\nconst getFullClassSelector = (element: Element): string =>\n element.localName +\n (element.classList.length > 0\n ? Array.from(element.classList)\n .map((cls) => `.${CSS.escape(cls)}`)\n .join('')\n : '');\n"],"mappings":";;;;;;;AAsBA,MAAa,yBACX,SACA,EACE,wBAAwB,OACxB,oBAAoB,UACY,CAAC,MACxB;CAEX,IAAI,QAAQ,aAAa,KAAK,eAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,YAAY,gBAChB,aACA,yBAAyB,iBAC3B;CAGA,IAAI,UAAU,WAAW,GAAG,GAC1B,OAAO;CAGT,MAAM,SAAS,YAAY;CAC3B,MAAM,iBAAiB,SACnB,sBAAsB,QAAQ;EAC5B;EACA,uBAAuB;CACzB,CAAC,IACD;CAEJ,OAAO,iBAAiB,GAAG,eAAe,KAAK,cAAc;AAC/D;AAEA,MAAM,mBAAmB,SAAe,eAAe,UAAkB;CACvE,IAAI,QAAQ,aAAa,KAAK,cAC5B,OAAO;CAGT,MAAM,cAAc;CACpB,MAAM,KAAK,YAAY,aAAa,IAAI;CAGxC,IAAI,gBAAgB,IAES;MAAA,YAAY,cAAc,iBACnD,IAAI,IAAI,OAAO,EAAE,GAGE,CAAC,CAAC,WAAW,GAChC,OAAO,IAAI,IAAI,OAAO,EAAE;CAAA;CAI5B,IAAI,WAAW,qBAAqB,WAAW;CAE/C,MAAM,QAAQ,YAAY,WAAW;CAErC,IAAI,QAAQ,GACV,YAAY,cAAc,MAAM;CAGlC,OAAO;AACT;AAEA,MAAM,eAAe,YAAiC;CAEpD,IAAI,CAAC,QAAQ,eACX,OAAO;CAGT,MAAM,UAAU,MAAM,KAAK,QAAQ,SAAS;CAC5C,MAAM,WAAW,MAAM,KAAK,QAAQ,cAAc,QAAQ;CAW1D,IAR2B,SAAS,MACjC,YACC,YAAY,WACZ,QAAQ,cAAc,QAAQ,aAC9B,QAAQ,OAAO,QAAQ,QAAQ,UAAU,SAAS,GAAG,CAAC,CAIrC,GACnB,OAAO,SAAS,QAAQ,OAAO,IAAI;CAGrC,OAAO;AACT;AAEA,MAAM,wBAAwB,YAC5B,QAAQ,aACP,QAAQ,UAAU,SAAS,IACxB,MAAM,KAAK,QAAQ,SAAS,CAAC,CAC1B,KAAK,QAAQ,IAAI,IAAI,OAAO,GAAG,GAAG,CAAC,CACnC,KAAK,EAAE,IACV"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentelemetry/browser-instrumentation",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "OpenTelemetry browser instrumentations.",
5
5
  "keywords": [
6
6
  "opentelemetry",
@@ -50,26 +50,26 @@
50
50
  "build": "tsdown",
51
51
  "watch": "tsdown --watch --no-clean",
52
52
  "check-types": "tsc",
53
- "msw:init": "msw init public --save",
53
+ "msw:init": "msw init",
54
54
  "test": "vitest run",
55
55
  "test:watch": "vitest",
56
56
  "test:coverage": "vitest --coverage"
57
57
  },
58
58
  "dependencies": {
59
- "@opentelemetry/api-logs": "^0.222.0",
60
- "@opentelemetry/instrumentation": "^0.222.0",
59
+ "@opentelemetry/api-logs": "^0.223.0",
60
+ "@opentelemetry/instrumentation": "^0.223.0",
61
61
  "@opentelemetry/semantic-conventions": "^1.43.0",
62
- "web-vitals": "^6.2.1"
62
+ "web-vitals": "^6.2.2"
63
63
  },
64
64
  "peerDependencies": {
65
65
  "@opentelemetry/api": "^1.9.1"
66
66
  },
67
67
  "devDependencies": {
68
- "@opentelemetry/core": "2.11.0",
69
- "@opentelemetry/propagator-b3": "2.11.0",
70
- "@opentelemetry/sdk-logs": "0.222.0",
71
- "@opentelemetry/sdk-trace-base": "2.11.0",
72
- "msw": "2.15.0"
68
+ "@opentelemetry/core": "2.12.0",
69
+ "@opentelemetry/propagator-b3": "2.12.0",
70
+ "@opentelemetry/sdk-logs": "0.223.0",
71
+ "@opentelemetry/sdk-trace-base": "2.12.0",
72
+ "msw": "3.0.1"
73
73
  },
74
74
  "publishConfig": {
75
75
  "access": "public"