@opentelemetry/sdk-trace-web
Version:
339 lines • 13.6 kB
JavaScript
/*
* 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
;