/*! @azure/msal-common v16.6.2 2026-05-19 */
'use strict';
import { getAndFlushLogsFromCache } from '../../logger/Logger.mjs';
import { IntFields, PerformanceEventStatus, EXT_FIELD_PREFIX } from './PerformanceEvent.mjs';
import { AuthError } from '../../error/AuthError.mjs';
import { CacheError } from '../../error/CacheError.mjs';
import { ServerError } from '../../error/ServerError.mjs';
import { InteractionRequiredAuthError } from '../../error/InteractionRequiredAuthError.mjs';

/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */
/**
 * Starts context by adding payload to the stack
 * @param event {PerformanceEvent}
 * @param stack {?PerformanceEventStackedContext[]} stack
 */
function startContext(event, stack) {
    if (!stack) {
        return;
    }
    stack.push({
        name: event.name,
    });
}
/**
 * Ends context by removing payload from the stack and returning parent or self, if stack is empty, payload
 *
 * @param event {PerformanceEvent}
 * @param stack {?PerformanceEventStackedContext[]} stack
 * @param error {?unknown} error
 */
function endContext(event, stack, error) {
    if (!stack?.length) {
        return;
    }
    const peek = (stack) => {
        return stack.length ? stack[stack.length - 1] : undefined;
    };
    const abbrEventName = event.name;
    const top = peek(stack);
    if (top?.name !== abbrEventName) {
        return;
    }
    const current = stack?.pop();
    if (!current) {
        return;
    }
    const errorCode = error instanceof AuthError
        ? error.errorCode
        : error instanceof Error
            ? error.name
            : undefined;
    const subErr = error instanceof AuthError ? error.subError : undefined;
    if (errorCode && current.childErr !== errorCode) {
        current.err = errorCode;
        if (subErr) {
            current.subErr = subErr;
        }
    }
    delete current.name;
    delete current.childErr;
    const context = {
        ...current,
        dur: event.durationMs,
    };
    if (!event.success) {
        context.fail = 1;
    }
    const parent = peek(stack);
    if (!parent) {
        return { [abbrEventName]: context };
    }
    if (errorCode) {
        parent.childErr = errorCode;
    }
    let childName;
    if (!parent[abbrEventName]) {
        childName = abbrEventName;
    }
    else {
        const siblings = Object.keys(parent).filter((key) => key.startsWith(abbrEventName)).length;
        childName = `${abbrEventName}_${siblings + 1}`;
    }
    parent[childName] = context;
    return parent;
}
/**
 * Adds error name and stack trace to the telemetry event
 * @param error {Error}
 * @param logger {Logger}
 * @param event {PerformanceEvent}
 * @param stackMaxSize {number} max error stack size to capture
 */
function addError(error, logger, event, stackMaxSize = 5) {
    if (!(error instanceof Error)) {
        logger.trace("PerformanceClient.addErrorStack: Input error is not instance of Error", event.correlationId);
        return;
    }
    else if (error instanceof AuthError) {
        event.errorCode = error.errorCode;
        event.subErrorCode = error.subError;
        if (!event.serverErrorNo &&
            (error instanceof ServerError ||
                error instanceof InteractionRequiredAuthError) &&
            error.errorNo) {
            event.serverErrorNo = error.errorNo;
        }
        return;
    }
    else if (error instanceof CacheError) {
        event.errorCode = error.errorCode;
        return;
    }
    else if (event.errorStack?.length) {
        logger.trace("PerformanceClient.addErrorStack: Stack already exist", event.correlationId);
        return;
    }
    else if (!error.stack?.length) {
        logger.trace("PerformanceClient.addErrorStack: Input stack is empty", event.correlationId);
        return;
    }
    if (error.stack) {
        event.errorStack = compactStack(error.stack, stackMaxSize);
    }
    event.errorName = error.name;
}
/**
 * Compacts error stack into array by fetching N first entries
 * @param stack {string} error stack
 * @param stackMaxSize {number} max error stack size to capture
 * @returns {string[]}
 */
function compactStack(stack, stackMaxSize) {
    if (stackMaxSize < 0) {
        return [];
    }
    const stackArr = stack.split("\n") || [];
    const res = [];
    // Check for a handful of known, common runtime errors and log them (with redaction where applicable).
    const firstLine = stackArr[0];
    if (firstLine.startsWith("TypeError: Cannot read property") ||
        firstLine.startsWith("TypeError: Cannot read properties of") ||
        firstLine.startsWith("TypeError: Cannot set property") ||
        firstLine.startsWith("TypeError: Cannot set properties of") ||
        firstLine.endsWith("is not a function")) {
        // These types of errors are not at risk of leaking PII. They will indicate unavailable APIs
        res.push(compactStackLine(firstLine));
    }
    else if (firstLine.startsWith("SyntaxError") ||
        firstLine.startsWith("TypeError")) {
        // Prevent unintentional leaking of arbitrary info by redacting contents between both single and double quotes
        res.push(compactStackLine(
        // Example: SyntaxError: Unexpected token 'e', "test" is not valid JSON -> SyntaxError: Unexpected token <redacted>, <redacted> is not valid JSON
        firstLine.replace(/['].*[']|["].*["]/g, "<redacted>")));
    }
    // Get top N stack lines
    for (let ix = 1; ix < stackArr.length; ix++) {
        if (res.length >= stackMaxSize) {
            break;
        }
        const line = stackArr[ix];
        res.push(compactStackLine(line));
    }
    return res;
}
/**
 * Compacts error stack line by shortening file path
 * Example: https://localhost/msal-common/src/authority/Authority.js:100:1 -> Authority.js:100:1
 * @param line {string} stack line
 * @returns {string}
 */
function compactStackLine(line) {
    const filePathIx = line.lastIndexOf(" ") + 1;
    if (filePathIx < 1) {
        return line;
    }
    const filePath = line.substring(filePathIx);
    let fileNameIx = filePath.lastIndexOf("/");
    fileNameIx = fileNameIx < 0 ? filePath.lastIndexOf("\\") : fileNameIx;
    if (fileNameIx >= 0) {
        return (line.substring(0, filePathIx) +
            "(" +
            filePath.substring(fileNameIx + 1) +
            (filePath.charAt(filePath.length - 1) === ")" ? "" : ")")).trimStart();
    }
    return line.trimStart();
}
function getAccountType(account) {
    const idTokenClaims = account?.idTokenClaims;
    if (idTokenClaims?.tfp || idTokenClaims?.acr) {
        return "B2C";
    }
    if (!idTokenClaims?.tid) {
        return undefined;
    }
    else if (idTokenClaims?.tid === "9188040d-6c67-4c5b-b112-36a304b66dad") {
        return "MSA";
    }
    return "AAD";
}
class PerformanceClient {
    /**
     * Creates an instance of PerformanceClient,
     * an abstract class containing core performance telemetry logic.
     *
     * @constructor
     * @param {string} clientId Client ID of the application
     * @param {string} authority Authority used by the application
     * @param {Logger} logger Logger used by the application
     * @param {string} libraryName Name of the library
     * @param {string} libraryVersion Version of the library
     * @param {ApplicationTelemetry} applicationTelemetry application name and version
     * @param {Set<String>} intFields integer fields to be truncated
     */
    constructor(clientId, authority, logger, libraryName, libraryVersion, applicationTelemetry, intFields) {
        this.authority = authority;
        this.libraryName = libraryName;
        this.libraryVersion = libraryVersion;
        this.applicationTelemetry = applicationTelemetry;
        this.clientId = clientId;
        this.logger = logger;
        this.callbacks = new Map();
        this.eventsByCorrelationId = new Map();
        this.eventStack = new Map();
        this.intFields = intFields || new Set();
        for (const item of IntFields) {
            this.intFields.add(item);
        }
    }
    /**
     * Starts measuring performance for a given operation. Returns a function that should be used to end the measurement.
     *
     * @param {PerformanceEvents} measureName
     * @param {?string} [correlationId]
     * @returns {InProgressPerformanceEvent}
     */
    startMeasurement(measureName, correlationId) {
        // Generate a placeholder correlation if the request does not provide one
        const eventCorrelationId = correlationId || this.generateId();
        const inProgressEvent = {
            eventId: this.generateId(),
            status: PerformanceEventStatus.InProgress,
            authority: this.authority,
            libraryName: this.libraryName,
            libraryVersion: this.libraryVersion,
            clientId: this.clientId,
            name: measureName,
            startTimeMs: Date.now(),
            correlationId: eventCorrelationId,
            appName: this.applicationTelemetry?.appName,
            appVersion: this.applicationTelemetry?.appVersion,
        };
        // Store in progress events so they can be discarded if not ended properly
        this.cacheEventByCorrelationId(inProgressEvent);
        startContext(inProgressEvent, this.eventStack.get(eventCorrelationId));
        // Return the event and functions the caller can use to properly end/flush the measurement
        return {
            end: (event, error, account) => {
                return this.endMeasurement({
                    // Initial set of event properties
                    ...inProgressEvent,
                    // Properties set when event ends
                    ...event,
                }, error, account);
            },
            discard: () => {
                return this.discardMeasurements(inProgressEvent.correlationId);
            },
            add: (fields) => {
                return this.addFields(fields, inProgressEvent.correlationId);
            },
            increment: (fields) => {
                return this.incrementFields(fields, inProgressEvent.correlationId);
            },
            event: inProgressEvent,
        };
    }
    /**
     * Stops measuring the performance for an operation. Should only be called directly by PerformanceClient classes,
     * as consumers should instead use the function returned by startMeasurement.
     * Adds a new field named as "[event name]DurationMs" for sub-measurements, completes and emits an event
     * otherwise.
     *
     * @param {PerformanceEvent} event
     * @param {unknown} error
     * @param {AccountInfo?} account
     * @returns {(PerformanceEvent | null)}
     */
    endMeasurement(event, error, account) {
        const rootEvent = this.eventsByCorrelationId.get(event.correlationId);
        if (!rootEvent) {
            this.logger.trace(`PerformanceClient: Measurement not found for '${event.eventId}'`, event.correlationId);
            return null;
        }
        const isRoot = event.eventId === rootEvent.eventId;
        event.durationMs = Math.round(event.durationMs || this.getDurationMs(event.startTimeMs));
        const context = JSON.stringify(endContext(event, this.eventStack.get(rootEvent.correlationId), error));
        if (isRoot) {
            this.discardMeasurements(rootEvent.correlationId);
        }
        else {
            rootEvent.incompleteSubMeasurements?.delete(event.eventId);
        }
        if (error) {
            addError(error, this.logger, rootEvent);
        }
        // Add sub-measurement attribute to root event's ext field.
        if (!isRoot) {
            rootEvent.ext = {
                ...rootEvent.ext,
                ...event.ext,
            };
            rootEvent.ext[event.name + "DurationMs"] = Math.floor(event.durationMs);
            return { ...rootEvent };
        }
        if (isRoot &&
            !error &&
            (rootEvent.errorCode || rootEvent.subErrorCode)) {
            this.logger.trace(`PerformanceClient: Remove error and sub-error codes for root event '${event.name}' as intermediate error was successfully handled`, event.correlationId);
            rootEvent.errorCode = undefined;
            rootEvent.subErrorCode = undefined;
        }
        let finalEvent = { ...rootEvent, ...event };
        let incompleteSubsCount = 0;
        // Incomplete sub-measurements are discarded. They are likely an instrumentation bug that should be fixed.
        finalEvent.incompleteSubMeasurements?.forEach((subMeasurement) => {
            this.logger.trace(`PerformanceClient: Incomplete submeasurement '${subMeasurement.name}' found for '${event.name}'`, finalEvent.correlationId);
            incompleteSubsCount++;
        });
        finalEvent.incompleteSubMeasurements = undefined;
        const logs = getAndFlushLogsFromCache(event.correlationId);
        // Format logs: [millis1,hash1;millis2,hash2;...]
        const formattedLogs = logs
            .map((logMessage) => `${logMessage.milliseconds},${logMessage.hash}`)
            .join(";");
        finalEvent = {
            ...finalEvent,
            status: PerformanceEventStatus.Completed,
            incompleteSubsCount,
            context,
            logs: formattedLogs,
        };
        if (account) {
            finalEvent.accountType = getAccountType(account);
            finalEvent.dataBoundary = account.dataBoundary;
        }
        this.truncateIntegralFields(finalEvent);
        this.emitEvents([finalEvent], event.correlationId);
        return finalEvent;
    }
    /**
     * Saves extra information to be emitted when the measurements are flushed
     * @param fields
     * @param correlationId
     */
    addFields(fields, correlationId) {
        const event = this.eventsByCorrelationId.get(correlationId);
        if (event) {
            const staticFields = {};
            const dynamicFields = {};
            for (const key in fields) {
                if (key.startsWith(EXT_FIELD_PREFIX)) {
                    const dynamicKey = key.slice(EXT_FIELD_PREFIX.length);
                    const value = fields[key];
                    if (typeof value === "string" ||
                        typeof value === "number") {
                        dynamicFields[dynamicKey] = value;
                    }
                }
                else {
                    staticFields[key] = fields[key];
                }
            }
            const updatedEvent = {
                ...event,
                ...staticFields,
            };
            if (Object.keys(dynamicFields).length) {
                updatedEvent.ext = {
                    ...updatedEvent.ext,
                    ...dynamicFields,
                };
            }
            this.eventsByCorrelationId.set(correlationId, updatedEvent);
        }
        else {
            this.logger.trace("PerformanceClient: Event not found for", correlationId);
        }
    }
    /**
     * Increment counters to be emitted when the measurements are flushed
     * @param fields {string[]}
     * @param correlationId {string} correlation identifier
     */
    incrementFields(fields, correlationId) {
        const event = this.eventsByCorrelationId.get(correlationId);
        if (event) {
            for (const counter in fields) {
                if (counter.startsWith(EXT_FIELD_PREFIX)) {
                    event.ext = event.ext || {};
                    // Route to ext sub-object
                    const dynamicKey = counter.slice(EXT_FIELD_PREFIX.length);
                    const currentValue = event.ext[dynamicKey];
                    if (currentValue === undefined) {
                        event.ext[dynamicKey] = 0;
                    }
                    else if (isNaN(Number(currentValue))) {
                        return;
                    }
                    event.ext[dynamicKey] =
                        (Number(event.ext[dynamicKey]) || 0) +
                            (fields[counter] ?? 0);
                }
                else {
                    /* eslint-disable custom-msal/no-dynamic-telemetry-fields -- internal dispatching of static fields by name */
                    if (!event.hasOwnProperty(counter)) {
                        event[counter] = 0;
                    }
                    else if (isNaN(Number(event[counter]))) {
                        return;
                    }
                    event[counter] += fields[counter];
                    /* eslint-enable custom-msal/no-dynamic-telemetry-fields */
                }
            }
        }
        else {
            this.logger.trace("PerformanceClient: Event not found for", correlationId);
        }
    }
    /**
     * Upserts event into event cache.
     * First key is the correlation id, second key is the event id.
     * Allows for events to be grouped by correlation id,
     * and to easily allow for properties on them to be updated.
     *
     * @private
     * @param {PerformanceEvent} event
     */
    cacheEventByCorrelationId(event) {
        const rootEvent = this.eventsByCorrelationId.get(event.correlationId);
        if (rootEvent) {
            rootEvent.incompleteSubMeasurements =
                rootEvent.incompleteSubMeasurements || new Map();
            rootEvent.incompleteSubMeasurements.set(event.eventId, {
                name: event.name,
                startTimeMs: event.startTimeMs,
            });
        }
        else {
            this.eventsByCorrelationId.set(event.correlationId, { ...event });
            this.eventStack.set(event.correlationId, []);
        }
    }
    /**
     * Removes measurements and aux data for a given correlation id.
     *
     * @param {string} correlationId
     */
    discardMeasurements(correlationId) {
        this.eventsByCorrelationId.delete(correlationId);
        this.eventStack.delete(correlationId);
    }
    /**
     * Registers a callback function to receive performance events.
     *
     * @param {PerformanceCallbackFunction} callback
     * @returns {string}
     */
    addPerformanceCallback(callback) {
        for (const [id, cb] of this.callbacks) {
            if (cb.toString() === callback.toString()) {
                this.logger.warning(`PerformanceClient: Performance callback is already registered with id: ${id}`, "");
                return id;
            }
        }
        const callbackId = this.generateId();
        this.callbacks.set(callbackId, callback);
        this.logger.verbose(`PerformanceClient: Performance callback registered with id: '${callbackId}'`, "");
        return callbackId;
    }
    /**
     * Removes a callback registered with addPerformanceCallback.
     *
     * @param {string} callbackId
     * @returns {boolean}
     */
    removePerformanceCallback(callbackId) {
        const result = this.callbacks.delete(callbackId);
        if (result) {
            this.logger.verbose(`PerformanceClient: Performance callback '${callbackId}' removed.`, "");
        }
        else {
            this.logger.verbose(`PerformanceClient: Performance callback '${callbackId}' not removed.`, "");
        }
        return result;
    }
    /**
     * Emits events to all registered callbacks.
     *
     * @param {PerformanceEvent[]} events
     * @param {?string} [correlationId]
     */
    emitEvents(events, correlationId) {
        this.logger.verbose("PerformanceClient: Emitting performance events", correlationId);
        this.callbacks.forEach((callback, callbackId) => {
            this.logger.trace(`PerformanceClient: Emitting event to callback '${callbackId}'`, correlationId);
            callback.apply(null, [events]);
        });
    }
    /**
     * Enforce truncation of integral fields in performance event.
     * @param {PerformanceEvent} event performance event to update.
     */
    truncateIntegralFields(event) {
        this.intFields.forEach((key) => {
            /* eslint-disable custom-msal/no-dynamic-telemetry-fields -- internal truncation of known integer fields */
            if (key in event && typeof event[key] === "number") {
                event[key] = Math.floor(event[key]);
            }
            /* eslint-enable custom-msal/no-dynamic-telemetry-fields */
        });
    }
    /**
     * Returns event duration in milliseconds
     * @param startTimeMs {number}
     * @returns {number}
     */
    getDurationMs(startTimeMs) {
        const durationMs = Date.now() - startTimeMs;
        // Handle clock skew
        return durationMs < 0 ? durationMs : 0;
    }
}

export { PerformanceClient, addError, compactStack, compactStackLine, endContext, getAccountType, startContext };
//# sourceMappingURL=PerformanceClient.mjs.map
