UNPKG

@opentelemetry/sdk-trace-web

Version:
339 lines 13.6 kB
"use strict"; /* * Copyright The OpenTelemetry Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.shouldPropagateTraceHeaders = exports.getElementXPath = exports.normalizeUrl = exports.parseUrl = exports.getResource = exports.sortResources = exports.addSpanNetworkEvents = exports.addSpanNetworkEvent = exports.hasKey = void 0; const PerformanceTimingNames_1 = require("./enums/PerformanceTimingNames"); const core_1 = require("@opentelemetry/core"); const semconv_1 = require("./semconv"); // Used to normalize relative URLs let urlNormalizingAnchor; function getUrlNormalizingAnchor() { if (!urlNormalizingAnchor) { urlNormalizingAnchor = document.createElement('a'); } return urlNormalizingAnchor; } /** * Helper function to be able to use enum as typed key in type and in interface when using forEach * @param obj * @param key */ function hasKey(obj, key) { return key in obj; } exports.hasKey = hasKey; /** * Helper function for starting an event on span based on {@link PerformanceEntries} * @param span * @param performanceName name of performance entry for time start * @param entries * @param ignoreZeros */ function addSpanNetworkEvent(span, performanceName, entries, ignoreZeros = true) { if (hasKey(entries, performanceName) && typeof entries[performanceName] === 'number' && !(ignoreZeros && entries[performanceName] === 0)) { return span.addEvent(performanceName, entries[performanceName]); } return undefined; } exports.addSpanNetworkEvent = addSpanNetworkEvent; /** * Helper function for adding network events and content length attributes. */ function addSpanNetworkEvents(span, resource, ignoreNetworkEvents = false, ignoreZeros, skipOldSemconvContentLengthAttrs) { if (ignoreZeros === undefined) { ignoreZeros = resource[PerformanceTimingNames_1.PerformanceTimingNames.START_TIME] !== 0; } if (!ignoreNetworkEvents) { addSpanNetworkEvent(span, PerformanceTimingNames_1.PerformanceTimingNames.FETCH_START, resource, ignoreZeros); addSpanNetworkEvent(span, PerformanceTimingNames_1.PerformanceTimingNames.DOMAIN_LOOKUP_START, resource, ignoreZeros); addSpanNetworkEvent(span, PerformanceTimingNames_1.PerformanceTimingNames.DOMAIN_LOOKUP_END, resource, ignoreZeros); addSpanNetworkEvent(span, PerformanceTimingNames_1.PerformanceTimingNames.CONNECT_START, resource, ignoreZeros); addSpanNetworkEvent(span, PerformanceTimingNames_1.PerformanceTimingNames.SECURE_CONNECTION_START, resource, ignoreZeros); addSpanNetworkEvent(span, PerformanceTimingNames_1.PerformanceTimingNames.CONNECT_END, resource, ignoreZeros); addSpanNetworkEvent(span, PerformanceTimingNames_1.PerformanceTimingNames.REQUEST_START, resource, ignoreZeros); addSpanNetworkEvent(span, PerformanceTimingNames_1.PerformanceTimingNames.RESPONSE_START, resource, ignoreZeros); addSpanNetworkEvent(span, PerformanceTimingNames_1.PerformanceTimingNames.RESPONSE_END, resource, ignoreZeros); } if (!skipOldSemconvContentLengthAttrs) { // This block adds content-length-related span attributes using the // *old* HTTP semconv (v1.7.0). const encodedLength = resource[PerformanceTimingNames_1.PerformanceTimingNames.ENCODED_BODY_SIZE]; if (encodedLength !== undefined) { span.setAttribute(semconv_1.ATTR_HTTP_RESPONSE_CONTENT_LENGTH, encodedLength); } const decodedLength = resource[PerformanceTimingNames_1.PerformanceTimingNames.DECODED_BODY_SIZE]; // Spec: Not set if transport encoding not used (in which case encoded and decoded sizes match) if (decodedLength !== undefined && encodedLength !== decodedLength) { span.setAttribute(semconv_1.ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED, decodedLength); } } } exports.addSpanNetworkEvents = addSpanNetworkEvents; /** * sort resources by startTime * @param filteredResources */ function sortResources(filteredResources) { return filteredResources.slice().sort((a, b) => { const valueA = a[PerformanceTimingNames_1.PerformanceTimingNames.FETCH_START]; const valueB = b[PerformanceTimingNames_1.PerformanceTimingNames.FETCH_START]; if (valueA > valueB) { return 1; } else if (valueA < valueB) { return -1; } return 0; }); } exports.sortResources = sortResources; /** Returns the origin if present (if in browser context). */ function getOrigin() { return typeof location !== 'undefined' ? location.origin : undefined; } /** * Get closest performance resource ignoring the resources that have been * already used. * @param spanUrl * @param startTimeHR * @param endTimeHR * @param resources * @param ignoredResources * @param initiatorType */ function getResource(spanUrl, startTimeHR, endTimeHR, resources, ignoredResources = new WeakSet(), initiatorType) { // de-relativize the URL before usage (does no harm to absolute URLs) const parsedSpanUrl = parseUrl(spanUrl); spanUrl = parsedSpanUrl.toString(); const filteredResources = filterResourcesForSpan(spanUrl, startTimeHR, endTimeHR, resources, ignoredResources, initiatorType); if (filteredResources.length === 0) { return { mainRequest: undefined, }; } if (filteredResources.length === 1) { return { mainRequest: filteredResources[0], }; } const sorted = sortResources(filteredResources); if (parsedSpanUrl.origin !== getOrigin() && sorted.length > 1) { let corsPreFlightRequest = sorted[0]; let mainRequest = findMainRequest(sorted, corsPreFlightRequest[PerformanceTimingNames_1.PerformanceTimingNames.RESPONSE_END], endTimeHR); const responseEnd = corsPreFlightRequest[PerformanceTimingNames_1.PerformanceTimingNames.RESPONSE_END]; const fetchStart = mainRequest[PerformanceTimingNames_1.PerformanceTimingNames.FETCH_START]; // no corsPreFlightRequest if (fetchStart < responseEnd) { mainRequest = corsPreFlightRequest; corsPreFlightRequest = undefined; } return { corsPreFlightRequest, mainRequest, }; } else { return { mainRequest: filteredResources[0], }; } } exports.getResource = getResource; /** * Will find the main request skipping the cors pre flight requests * @param resources * @param corsPreFlightRequestEndTime * @param spanEndTimeHR */ function findMainRequest(resources, corsPreFlightRequestEndTime, spanEndTimeHR) { const spanEndTime = (0, core_1.hrTimeToNanoseconds)(spanEndTimeHR); const minTime = (0, core_1.hrTimeToNanoseconds)((0, core_1.timeInputToHrTime)(corsPreFlightRequestEndTime)); let mainRequest = resources[1]; let bestGap; const length = resources.length; for (let i = 1; i < length; i++) { const resource = resources[i]; const resourceStartTime = (0, core_1.hrTimeToNanoseconds)((0, core_1.timeInputToHrTime)(resource[PerformanceTimingNames_1.PerformanceTimingNames.FETCH_START])); const resourceEndTime = (0, core_1.hrTimeToNanoseconds)((0, core_1.timeInputToHrTime)(resource[PerformanceTimingNames_1.PerformanceTimingNames.RESPONSE_END])); const currentGap = spanEndTime - resourceEndTime; if (resourceStartTime >= minTime && (!bestGap || currentGap < bestGap)) { bestGap = currentGap; mainRequest = resource; } } return mainRequest; } /** * Filter all resources that has started and finished according to span start time and end time. * It will return the closest resource to a start time * @param spanUrl * @param startTimeHR * @param endTimeHR * @param resources * @param ignoredResources */ function filterResourcesForSpan(spanUrl, startTimeHR, endTimeHR, resources, ignoredResources, initiatorType) { const startTime = (0, core_1.hrTimeToNanoseconds)(startTimeHR); const endTime = (0, core_1.hrTimeToNanoseconds)(endTimeHR); let filteredResources = resources.filter(resource => { const resourceStartTime = (0, core_1.hrTimeToNanoseconds)((0, core_1.timeInputToHrTime)(resource[PerformanceTimingNames_1.PerformanceTimingNames.FETCH_START])); const resourceEndTime = (0, core_1.hrTimeToNanoseconds)((0, core_1.timeInputToHrTime)(resource[PerformanceTimingNames_1.PerformanceTimingNames.RESPONSE_END])); return (resource.initiatorType.toLowerCase() === (initiatorType || 'xmlhttprequest') && resource.name === spanUrl && resourceStartTime >= startTime && resourceEndTime <= endTime); }); if (filteredResources.length > 0) { filteredResources = filteredResources.filter(resource => { return !ignoredResources.has(resource); }); } return filteredResources; } /** * Parses url using URL constructor or fallback to anchor element. * @param url */ function parseUrl(url) { if (typeof URL === 'function') { return new URL(url, typeof document !== 'undefined' ? document.baseURI : typeof location !== 'undefined' // Some JS runtimes (e.g. Deno) don't define this ? location.href : undefined); } const element = getUrlNormalizingAnchor(); element.href = url; return element; } exports.parseUrl = parseUrl; /** * Parses url using URL constructor or fallback to anchor element and serialize * it to a string. * * Performs the steps described in https://html.spec.whatwg.org/multipage/urls-and-fetching.html#parse-a-url * * @param url */ function normalizeUrl(url) { const urlLike = parseUrl(url); return urlLike.href; } exports.normalizeUrl = normalizeUrl; /** * Get element XPath * @param target - target element * @param optimised - when id attribute of element is present the xpath can be * simplified to contain id */ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types function getElementXPath(target, optimised) { if (target.nodeType === Node.DOCUMENT_NODE) { return '/'; } const targetValue = getNodeValue(target, optimised); if (optimised && targetValue.indexOf('@id') > 0) { return targetValue; } let xpath = ''; if (target.parentNode) { xpath += getElementXPath(target.parentNode, false); } xpath += targetValue; return xpath; } exports.getElementXPath = getElementXPath; /** * get node index within the siblings * @param target */ function getNodeIndex(target) { if (!target.parentNode) { return 0; } const allowedTypes = [target.nodeType]; if (target.nodeType === Node.CDATA_SECTION_NODE) { allowedTypes.push(Node.TEXT_NODE); } let elements = Array.from(target.parentNode.childNodes); elements = elements.filter((element) => { const localName = element.localName; return (allowedTypes.indexOf(element.nodeType) >= 0 && localName === target.localName); }); if (elements.length >= 1) { return elements.indexOf(target) + 1; // xpath starts from 1 } // if there are no other similar child xpath doesn't need index return 0; } /** * get node value for xpath * @param target * @param optimised */ function getNodeValue(target, optimised) { const nodeType = target.nodeType; const index = getNodeIndex(target); let nodeValue = ''; if (nodeType === Node.ELEMENT_NODE) { const id = target.getAttribute('id'); if (optimised && id) { return `//*[@id="${id}"]`; } nodeValue = target.localName; } else if (nodeType === Node.TEXT_NODE || nodeType === Node.CDATA_SECTION_NODE) { nodeValue = 'text()'; } else if (nodeType === Node.COMMENT_NODE) { nodeValue = 'comment()'; } else { return ''; } // if index is 1 it can be omitted in xpath if (nodeValue && index > 1) { return `/${nodeValue}[${index}]`; } return `/${nodeValue}`; } /** * Checks if trace headers should be propagated * @param spanUrl * @private */ function shouldPropagateTraceHeaders(spanUrl, propagateTraceHeaderCorsUrls) { let propagateTraceHeaderUrls = propagateTraceHeaderCorsUrls || []; if (typeof propagateTraceHeaderUrls === 'string' || propagateTraceHeaderUrls instanceof RegExp) { propagateTraceHeaderUrls = [propagateTraceHeaderUrls]; } const parsedSpanUrl = parseUrl(spanUrl); if (parsedSpanUrl.origin === getOrigin()) { return true; } else { return propagateTraceHeaderUrls.some(propagateTraceHeaderUrl => (0, core_1.urlMatches)(spanUrl, propagateTraceHeaderUrl)); } } exports.shouldPropagateTraceHeaders = shouldPropagateTraceHeaders; //# sourceMappingURL=utils.js.map