"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.FetchInstrumentation = void 0;
const api = require("@opentelemetry/api");
const instrumentation_1 = require("@opentelemetry/instrumentation");
const core = require("@opentelemetry/core");
const web = require("@opentelemetry/sdk-trace-web");
const AttributeNames_1 = require("./enums/AttributeNames");
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
const version_1 = require("./version");
const core_1 = require("@opentelemetry/core");
// how long to wait for observer to collect information about resources
// this is needed as event "load" is called before observer
// hard to say how long it should really wait, seems like 300ms is
// safe enough
const OBSERVER_WAIT_TIME_MS = 300;
/**
 * This class represents a fetch plugin for auto instrumentation
 */
class FetchInstrumentation extends instrumentation_1.InstrumentationBase {
    constructor(config) {
        super('@opentelemetry/instrumentation-fetch', version_1.VERSION, config);
        this.component = 'fetch';
        this.version = version_1.VERSION;
        this.moduleName = this.component;
        this._usedResources = new WeakSet();
        this._tasksCount = 0;
    }
    init() { }
    _getConfig() {
        return this._config;
    }
    /**
     * Add cors pre flight child span
     * @param span
     * @param corsPreFlightRequest
     */
    _addChildSpan(span, corsPreFlightRequest) {
        const childSpan = this.tracer.startSpan('CORS Preflight', {
            startTime: corsPreFlightRequest[web.PerformanceTimingNames.FETCH_START],
        }, api.trace.setSpan(api.context.active(), span));
        if (!this._getConfig().ignoreNetworkEvents) {
            web.addSpanNetworkEvents(childSpan, corsPreFlightRequest);
        }
        childSpan.end(corsPreFlightRequest[web.PerformanceTimingNames.RESPONSE_END]);
    }
    /**
     * Adds more attributes to span just before ending it
     * @param span
     * @param response
     */
    _addFinalSpanAttributes(span, response) {
        const parsedUrl = web.parseUrl(response.url);
        span.setAttribute(semantic_conventions_1.SemanticAttributes.HTTP_STATUS_CODE, response.status);
        if (response.statusText != null) {
            span.setAttribute(AttributeNames_1.AttributeNames.HTTP_STATUS_TEXT, response.statusText);
        }
        span.setAttribute(semantic_conventions_1.SemanticAttributes.HTTP_HOST, parsedUrl.host);
        span.setAttribute(semantic_conventions_1.SemanticAttributes.HTTP_SCHEME, parsedUrl.protocol.replace(':', ''));
        span.setAttribute(semantic_conventions_1.SemanticAttributes.HTTP_USER_AGENT, navigator.userAgent);
    }
    /**
     * Add headers
     * @param options
     * @param spanUrl
     */
    _addHeaders(options, spanUrl) {
        if (!web.shouldPropagateTraceHeaders(spanUrl, this._getConfig().propagateTraceHeaderCorsUrls)) {
            const headers = {};
            api.propagation.inject(api.context.active(), headers);
            if (Object.keys(headers).length > 0) {
                this._diag.debug('headers inject skipped due to CORS policy');
            }
            return;
        }
        if (options instanceof Request) {
            api.propagation.inject(api.context.active(), options.headers, {
                set: (h, k, v) => h.set(k, typeof v === 'string' ? v : String(v)),
            });
        }
        else if (options.headers instanceof Headers) {
            api.propagation.inject(api.context.active(), options.headers, {
                set: (h, k, v) => h.set(k, typeof v === 'string' ? v : String(v)),
            });
        }
        else {
            const headers = {};
            api.propagation.inject(api.context.active(), headers);
            options.headers = Object.assign({}, headers, options.headers || {});
        }
    }
    /**
     * Clears the resource timings and all resources assigned with spans
     *     when {@link FetchPluginConfig.clearTimingResources} is
     *     set to true (default false)
     * @private
     */
    _clearResources() {
        if (this._tasksCount === 0 && this._getConfig().clearTimingResources) {
            performance.clearResourceTimings();
            this._usedResources = new WeakSet();
        }
    }
    /**
     * Creates a new span
     * @param url
     * @param options
     */
    _createSpan(url, options = {}) {
        if (core.isUrlIgnored(url, this._getConfig().ignoreUrls)) {
            this._diag.debug('ignoring span as url matches ignored url');
            return;
        }
        const method = (options.method || 'GET').toUpperCase();
        const spanName = `HTTP ${method}`;
        return this.tracer.startSpan(spanName, {
            kind: api.SpanKind.CLIENT,
            attributes: {
                [AttributeNames_1.AttributeNames.COMPONENT]: this.moduleName,
                [semantic_conventions_1.SemanticAttributes.HTTP_METHOD]: method,
                [semantic_conventions_1.SemanticAttributes.HTTP_URL]: url,
            },
        });
    }
    /**
     * Finds appropriate resource and add network events to the span
     * @param span
     * @param resourcesObserver
     * @param endTime
     */
    _findResourceAndAddNetworkEvents(span, resourcesObserver, endTime) {
        let resources = resourcesObserver.entries;
        if (!resources.length) {
            if (!performance.getEntriesByType) {
                return;
            }
            // fallback - either Observer is not available or it took longer
            // then OBSERVER_WAIT_TIME_MS and observer didn't collect enough
            // information
            resources = performance.getEntriesByType('resource');
        }
        const resource = web.getResource(resourcesObserver.spanUrl, resourcesObserver.startTime, endTime, resources, this._usedResources, 'fetch');
        if (resource.mainRequest) {
            const mainRequest = resource.mainRequest;
            this._markResourceAsUsed(mainRequest);
            const corsPreFlightRequest = resource.corsPreFlightRequest;
            if (corsPreFlightRequest) {
                this._addChildSpan(span, corsPreFlightRequest);
                this._markResourceAsUsed(corsPreFlightRequest);
            }
            if (!this._getConfig().ignoreNetworkEvents) {
                web.addSpanNetworkEvents(span, mainRequest);
            }
        }
    }
    /**
     * Marks certain [resource]{@link PerformanceResourceTiming} when information
     * from this is used to add events to span.
     * This is done to avoid reusing the same resource again for next span
     * @param resource
     */
    _markResourceAsUsed(resource) {
        this._usedResources.add(resource);
    }
    /**
     * Finish span, add attributes, network events etc.
     * @param span
     * @param spanData
     * @param response
     */
    _endSpan(span, spanData, response) {
        const endTime = core.millisToHrTime(Date.now());
        const performanceEndTime = core.hrTime();
        this._addFinalSpanAttributes(span, response);
        setTimeout(() => {
            var _a;
            (_a = spanData.observer) === null || _a === void 0 ? void 0 : _a.disconnect();
            this._findResourceAndAddNetworkEvents(span, spanData, performanceEndTime);
            this._tasksCount--;
            this._clearResources();
            span.end(endTime);
        }, OBSERVER_WAIT_TIME_MS);
    }
    /**
     * Patches the constructor of fetch
     */
    _patchConstructor() {
        return original => {
            const plugin = this;
            return function patchConstructor(...args) {
                const self = this;
                const url = web.parseUrl(args[0] instanceof Request ? args[0].url : String(args[0])).href;
                const options = args[0] instanceof Request ? args[0] : args[1] || {};
                const createdSpan = plugin._createSpan(url, options);
                if (!createdSpan) {
                    return original.apply(this, args);
                }
                const spanData = plugin._prepareSpanData(url);
                function endSpanOnError(span, error) {
                    plugin._applyAttributesAfterFetch(span, options, error);
                    plugin._endSpan(span, spanData, {
                        status: error.status || 0,
                        statusText: error.message,
                        url,
                    });
                }
                function endSpanOnSuccess(span, response) {
                    plugin._applyAttributesAfterFetch(span, options, response);
                    if (response.status >= 200 && response.status < 400) {
                        plugin._endSpan(span, spanData, response);
                    }
                    else {
                        plugin._endSpan(span, spanData, {
                            status: response.status,
                            statusText: response.statusText,
                            url,
                        });
                    }
                }
                function onSuccess(span, resolve, response) {
                    try {
                        const resClone = response.clone();
                        const resClone4Hook = response.clone();
                        const body = resClone.body;
                        if (body) {
                            const reader = body.getReader();
                            const read = () => {
                                reader.read().then(({ done }) => {
                                    if (done) {
                                        endSpanOnSuccess(span, resClone4Hook);
                                    }
                                    else {
                                        read();
                                    }
                                }, error => {
                                    endSpanOnError(span, error);
                                });
                            };
                            read();
                        }
                        else {
                            // some older browsers don't have .body implemented
                            endSpanOnSuccess(span, response);
                        }
                    }
                    finally {
                        resolve(response);
                    }
                }
                function onError(span, reject, error) {
                    try {
                        endSpanOnError(span, error);
                    }
                    finally {
                        reject(error);
                    }
                }
                return new Promise((resolve, reject) => {
                    return api.context.with(api.trace.setSpan(api.context.active(), createdSpan), () => {
                        plugin._addHeaders(options, url);
                        plugin._tasksCount++;
                        // TypeScript complains about arrow function captured a this typed as globalThis
                        // ts(7041)
                        return original
                            .apply(self, options instanceof Request ? [options] : [url, options])
                            .then(onSuccess.bind(self, createdSpan, resolve), onError.bind(self, createdSpan, reject));
                    });
                });
            };
        };
    }
    _applyAttributesAfterFetch(span, request, result) {
        const applyCustomAttributesOnSpan = this._getConfig().applyCustomAttributesOnSpan;
        if (applyCustomAttributesOnSpan) {
            (0, instrumentation_1.safeExecuteInTheMiddle)(() => applyCustomAttributesOnSpan(span, request, result), error => {
                if (!error) {
                    return;
                }
                this._diag.error('applyCustomAttributesOnSpan', error);
            }, true);
        }
    }
    /**
     * Prepares a span data - needed later for matching appropriate network
     *     resources
     * @param spanUrl
     */
    _prepareSpanData(spanUrl) {
        const startTime = core.hrTime();
        const entries = [];
        if (typeof PerformanceObserver !== 'function') {
            return { entries, startTime, spanUrl };
        }
        const observer = new PerformanceObserver(list => {
            const perfObsEntries = list.getEntries();
            perfObsEntries.forEach(entry => {
                if (entry.initiatorType === 'fetch' && entry.name === spanUrl) {
                    entries.push(entry);
                }
            });
        });
        observer.observe({
            entryTypes: ['resource'],
        });
        return { entries, observer, startTime, spanUrl };
    }
    /**
     * implements enable function
     */
    enable() {
        if ((0, instrumentation_1.isWrapped)(fetch)) {
            this._unwrap(core_1._globalThis, 'fetch');
            this._diag.debug('removing previous patch for constructor');
        }
        this._wrap(core_1._globalThis, 'fetch', this._patchConstructor());
    }
    /**
     * implements unpatch function
     */
    disable() {
        this._unwrap(core_1._globalThis, 'fetch');
        this._usedResources = new WeakSet();
    }
}
exports.FetchInstrumentation = FetchInstrumentation;
//# sourceMappingURL=fetch.js.map