"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApolloServerPluginUsageReportingDisabled = exports.makeHTTPRequestHeaders = exports.ApolloServerPluginUsageReporting = void 0;
const os_1 = __importDefault(require("os"));
const zlib_1 = require("zlib");
const async_retry_1 = __importDefault(require("async-retry"));
const apollo_graphql_1 = require("apollo-graphql");
const apollo_reporting_protobuf_1 = require("apollo-reporting-protobuf");
const apollo_server_env_1 = require("apollo-server-env");
const signatureCache_1 = require("./signatureCache");
const traceTreeBuilder_1 = require("../traceTreeBuilder");
const traceDetails_1 = require("./traceDetails");
const graphql_1 = require("graphql");
const schemaReporting_1 = require("../schemaReporting");
const stats_1 = require("./stats");
const apollo_cache_control_1 = require("apollo-cache-control");
const defaultSendOperationsAsTrace_1 = require("./defaultSendOperationsAsTrace");
const reportHeaderDefaults = {
    hostname: os_1.default.hostname(),
    agentVersion: `apollo-server-core@${require('../../../package.json').version}`,
    runtimeVersion: `node ${process.version}`,
    uname: `${os_1.default.platform()}, ${os_1.default.type()}, ${os_1.default.release()}, ${os_1.default.arch()})`,
};
class ReportData {
    constructor(executableSchemaId, graphRef) {
        this.header = new apollo_reporting_protobuf_1.ReportHeader(Object.assign(Object.assign({}, reportHeaderDefaults), { executableSchemaId,
            graphRef }));
        this.reset();
    }
    reset() {
        this.report = new stats_1.OurReport(this.header);
    }
}
function ApolloServerPluginUsageReporting(options = Object.create(null)) {
    let requestDidStartHandler;
    return {
        __internal_plugin_id__() {
            return 'UsageReporting';
        },
        requestDidStart(requestContext) {
            if (!requestDidStartHandler) {
                throw Error('The usage reporting plugin has been asked to handle a request before the ' +
                    'server has started. See https://github.com/apollographql/apollo-server/issues/4588 ' +
                    'for more details.');
            }
            return requestDidStartHandler(requestContext);
        },
        serverWillStart({ logger: serverLogger, apollo, serverlessFramework, }) {
            var _a, _b, _c, _d;
            const logger = (_a = options.logger) !== null && _a !== void 0 ? _a : serverLogger;
            const { key, graphRef } = apollo;
            if (!(key && graphRef)) {
                throw new Error("You've enabled usage reporting via ApolloServerPluginUsageReporting, " +
                    'but you also need to provide your Apollo API key and graph ref, via ' +
                    'the APOLLO_KEY/APOLLO_GRAPH_REF environment ' +
                    'variables or via `new ApolloServer({apollo: {key, graphRef})`.');
            }
            logger.info('Apollo usage reporting starting! See your graph at ' +
                `https://studio.apollographql.com/graph/${encodeURI(graphRef)}/`);
            const sendReportsImmediately = (_b = options.sendReportsImmediately) !== null && _b !== void 0 ? _b : serverlessFramework;
            const signatureCache = signatureCache_1.createSignatureCache({ logger });
            const reportDataByExecutableSchemaId = Object.create(null);
            const overriddenExecutableSchemaId = options.overrideReportedSchema
                ? schemaReporting_1.computeCoreSchemaHash(options.overrideReportedSchema)
                : undefined;
            let lastSeenExecutableSchemaToId;
            let reportTimer;
            if (!sendReportsImmediately) {
                reportTimer = setInterval(() => sendAllReportsAndReportErrors(), options.reportIntervalMs || 10 * 1000);
            }
            let graphMightSupportTraces = true;
            const sendOperationAsTrace = (_c = options.experimental_sendOperationAsTrace) !== null && _c !== void 0 ? _c : defaultSendOperationsAsTrace_1.defaultSendOperationsAsTrace();
            const includeTracesContributingToStats = (_d = options.internal_includeTracesContributingToStats) !== null && _d !== void 0 ? _d : false;
            let stopped = false;
            function executableSchemaIdForSchema(schema) {
                if ((lastSeenExecutableSchemaToId === null || lastSeenExecutableSchemaToId === void 0 ? void 0 : lastSeenExecutableSchemaToId.executableSchema) === schema) {
                    return lastSeenExecutableSchemaToId.executableSchemaId;
                }
                const id = schemaReporting_1.computeCoreSchemaHash(graphql_1.printSchema(schema));
                lastSeenExecutableSchemaToId = {
                    executableSchema: schema,
                    executableSchemaId: id,
                };
                return id;
            }
            const getReportData = (executableSchemaId) => {
                const existing = reportDataByExecutableSchemaId[executableSchemaId];
                if (existing) {
                    return existing;
                }
                const reportData = new ReportData(executableSchemaId, graphRef);
                reportDataByExecutableSchemaId[executableSchemaId] = reportData;
                return reportData;
            };
            function sendAllReportsAndReportErrors() {
                return __awaiter(this, void 0, void 0, function* () {
                    yield Promise.all(Object.keys(reportDataByExecutableSchemaId).map((executableSchemaId) => sendReportAndReportErrors(executableSchemaId)));
                });
            }
            function sendReportAndReportErrors(executableSchemaId) {
                return __awaiter(this, void 0, void 0, function* () {
                    return sendReport(executableSchemaId).catch((err) => {
                        if (options.reportErrorFunction) {
                            options.reportErrorFunction(err);
                        }
                        else {
                            logger.error(err.message);
                        }
                    });
                });
            }
            const sendReport = (executableSchemaId) => __awaiter(this, void 0, void 0, function* () {
                var _e, _f;
                const reportData = getReportData(executableSchemaId);
                const { report } = reportData;
                reportData.reset();
                if (Object.keys(report.tracesPerQuery).length === 0) {
                    return;
                }
                report.endTime = traceTreeBuilder_1.dateToProtoTimestamp(new Date());
                const protobufError = apollo_reporting_protobuf_1.Report.verify(report);
                if (protobufError) {
                    throw new Error(`Error encoding report: ${protobufError}`);
                }
                const message = apollo_reporting_protobuf_1.Report.encode(report).finish();
                if (options.debugPrintReports) {
                    const decodedReport = apollo_reporting_protobuf_1.Report.decode(message);
                    logger.warn(`Apollo usage report: ${JSON.stringify(decodedReport.toJSON())}`);
                }
                const compressed = yield new Promise((resolve, reject) => {
                    const messageBuffer = Buffer.from(message.buffer, message.byteOffset, message.byteLength);
                    zlib_1.gzip(messageBuffer, (err, gzipResult) => {
                        if (err) {
                            reject(err);
                        }
                        else {
                            resolve(gzipResult);
                        }
                    });
                });
                const fetcher = (_e = options.fetcher) !== null && _e !== void 0 ? _e : apollo_server_env_1.fetch;
                const response = yield async_retry_1.default(() => __awaiter(this, void 0, void 0, function* () {
                    const curResponse = yield fetcher((options.endpointUrl ||
                        'https://usage-reporting.api.apollographql.com') +
                        '/api/ingress/traces', {
                        method: 'POST',
                        headers: {
                            'user-agent': 'ApolloServerPluginUsageReporting',
                            'x-api-key': key,
                            'content-encoding': 'gzip',
                            accept: 'application/json',
                        },
                        body: compressed,
                        agent: options.requestAgent,
                    });
                    if (curResponse.status >= 500 && curResponse.status < 600) {
                        throw new Error(`HTTP status ${curResponse.status}, ${(yield curResponse.text()) || '(no body)'}`);
                    }
                    else {
                        return curResponse;
                    }
                }), {
                    retries: (options.maxAttempts || 5) - 1,
                    minTimeout: options.minimumRetryDelayMs || 100,
                    factor: 2,
                }).catch((err) => {
                    throw new Error(`Error sending report to Apollo servers: ${err.message}`);
                });
                if (response.status < 200 || response.status >= 300) {
                    throw new Error(`Error sending report to Apollo servers: HTTP status ${response.status}, ${(yield response.text()) || '(no body)'}`);
                }
                if (graphMightSupportTraces &&
                    response.status === 200 && ((_f = response.headers
                    .get('content-type')) === null || _f === void 0 ? void 0 : _f.match(/^\s*application\/json\s*(?:;|$)/i))) {
                    const body = yield response.text();
                    let parsedBody;
                    try {
                        parsedBody = JSON.parse(body);
                    }
                    catch (e) {
                        throw new Error(`Error parsing response from Apollo servers: ${e}`);
                    }
                    if (parsedBody.tracesIgnored === true) {
                        logger.debug("This graph's organization does not have access to traces; sending all " +
                            'subsequent operations as traces.');
                        graphMightSupportTraces = false;
                    }
                }
                if (options.debugPrintReports) {
                    logger.warn(`Apollo usage report: status ${response.status}`);
                }
            });
            requestDidStartHandler = ({ logger: requestLogger, metrics, schema, request: { http, variables }, }) => {
                var _a;
                const logger = (_a = requestLogger !== null && requestLogger !== void 0 ? requestLogger : options.logger) !== null && _a !== void 0 ? _a : serverLogger;
                const treeBuilder = new traceTreeBuilder_1.TraceTreeBuilder({
                    rewriteError: options.rewriteError,
                    logger,
                });
                treeBuilder.startTiming();
                metrics.startHrTime = treeBuilder.startHrTime;
                let graphqlValidationFailure = false;
                let graphqlUnknownOperationName = false;
                if (http) {
                    treeBuilder.trace.http = new apollo_reporting_protobuf_1.Trace.HTTP({
                        method: apollo_reporting_protobuf_1.Trace.HTTP.Method[http.method] || apollo_reporting_protobuf_1.Trace.HTTP.Method.UNKNOWN,
                        host: null,
                        path: null,
                    });
                    if (options.sendHeaders) {
                        makeHTTPRequestHeaders(treeBuilder.trace.http, http.headers, options.sendHeaders);
                    }
                }
                function shouldIncludeRequest(requestContext) {
                    return __awaiter(this, void 0, void 0, function* () {
                        if (metrics.captureTraces !== undefined)
                            return;
                        if (typeof options.includeRequest !== 'function') {
                            metrics.captureTraces = true;
                            return;
                        }
                        metrics.captureTraces = yield options.includeRequest(requestContext);
                        if (typeof metrics.captureTraces !== 'boolean') {
                            logger.warn("The 'includeRequest' async predicate function must return a boolean value.");
                            metrics.captureTraces = true;
                        }
                    });
                }
                let endDone = false;
                function didEnd(requestContext) {
                    if (endDone)
                        return;
                    endDone = true;
                    treeBuilder.stopTiming();
                    if (metrics.captureTraces === undefined) {
                        logger.warn('captureTrace is undefined at the end of the request. This is a bug in ApolloServerPluginUsageReporting.');
                    }
                    if (metrics.captureTraces === false)
                        return;
                    treeBuilder.trace.fullQueryCacheHit = !!metrics.responseCacheHit;
                    treeBuilder.trace.forbiddenOperation = !!metrics.forbiddenOperation;
                    treeBuilder.trace.registeredOperation = !!metrics.registeredOperation;
                    if (requestContext.overallCachePolicy) {
                        treeBuilder.trace.cachePolicy = new apollo_reporting_protobuf_1.Trace.CachePolicy({
                            scope: requestContext.overallCachePolicy.scope === apollo_cache_control_1.CacheScope.Private
                                ? apollo_reporting_protobuf_1.Trace.CachePolicy.Scope.PRIVATE
                                : requestContext.overallCachePolicy.scope ===
                                    apollo_cache_control_1.CacheScope.Public
                                    ? apollo_reporting_protobuf_1.Trace.CachePolicy.Scope.PUBLIC
                                    : apollo_reporting_protobuf_1.Trace.CachePolicy.Scope.UNKNOWN,
                            maxAgeNs: requestContext.overallCachePolicy.maxAge * 1e9,
                        });
                    }
                    const operationName = requestContext.operationName ||
                        requestContext.request.operationName ||
                        '';
                    if (metrics.queryPlanTrace) {
                        treeBuilder.trace.queryPlan = metrics.queryPlanTrace;
                    }
                    addTrace().catch(logger.error);
                    function addTrace() {
                        return __awaiter(this, void 0, void 0, function* () {
                            if (stopped) {
                                return;
                            }
                            yield new Promise((res) => setImmediate(res));
                            const executableSchemaId = overriddenExecutableSchemaId !== null && overriddenExecutableSchemaId !== void 0 ? overriddenExecutableSchemaId : executableSchemaIdForSchema(schema);
                            const reportData = getReportData(executableSchemaId);
                            const { report } = reportData;
                            const { trace } = treeBuilder;
                            let statsReportKey = undefined;
                            if (!requestContext.document) {
                                statsReportKey = `## GraphQLParseFailure\n`;
                            }
                            else if (graphqlValidationFailure) {
                                statsReportKey = `## GraphQLValidationFailure\n`;
                            }
                            else if (graphqlUnknownOperationName) {
                                statsReportKey = `## GraphQLUnknownOperationName\n`;
                            }
                            if (statsReportKey) {
                                if (options.sendUnexecutableOperationDocuments) {
                                    trace.unexecutedOperationBody = requestContext.source;
                                    trace.unexecutedOperationName = operationName;
                                }
                            }
                            else {
                                const signature = getTraceSignature();
                                statsReportKey = `# ${operationName || '-'}\n${signature}`;
                            }
                            const protobufError = apollo_reporting_protobuf_1.Trace.verify(trace);
                            if (protobufError) {
                                throw new Error(`Error encoding trace: ${protobufError}`);
                            }
                            report.addTrace({
                                statsReportKey,
                                trace,
                                asTrace: graphMightSupportTraces &&
                                    sendOperationAsTrace(trace, statsReportKey),
                                includeTracesContributingToStats,
                            });
                            if (sendReportsImmediately ||
                                report.sizeEstimator.bytes >=
                                    (options.maxUncompressedReportSize || 4 * 1024 * 1024)) {
                                yield sendReportAndReportErrors(executableSchemaId);
                            }
                        });
                    }
                    function getTraceSignature() {
                        if (!requestContext.document) {
                            throw new Error('No document?');
                        }
                        const cacheKey = signatureCache_1.signatureCacheKey(requestContext.queryHash, operationName);
                        const cachedSignature = signatureCache.get(cacheKey);
                        if (cachedSignature) {
                            return cachedSignature;
                        }
                        const generatedSignature = (options.calculateSignature || apollo_graphql_1.defaultUsageReportingSignature)(requestContext.document, operationName);
                        signatureCache.set(cacheKey, generatedSignature);
                        return generatedSignature;
                    }
                }
                let didResolveSource = false;
                return {
                    didResolveSource(requestContext) {
                        didResolveSource = true;
                        if (metrics.persistedQueryHit) {
                            treeBuilder.trace.persistedQueryHit = true;
                        }
                        if (metrics.persistedQueryRegister) {
                            treeBuilder.trace.persistedQueryRegister = true;
                        }
                        if (variables) {
                            treeBuilder.trace.details = traceDetails_1.makeTraceDetails(variables, options.sendVariableValues, requestContext.source);
                        }
                        const clientInfo = (options.generateClientInfo || defaultGenerateClientInfo)(requestContext);
                        if (clientInfo) {
                            const { clientName, clientVersion, clientReferenceId, } = clientInfo;
                            treeBuilder.trace.clientVersion = clientVersion || '';
                            treeBuilder.trace.clientReferenceId = clientReferenceId || '';
                            treeBuilder.trace.clientName = clientName || '';
                        }
                    },
                    validationDidStart() {
                        return (validationErrors) => {
                            graphqlValidationFailure = validationErrors
                                ? validationErrors.length !== 0
                                : false;
                        };
                    },
                    didResolveOperation(requestContext) {
                        return __awaiter(this, void 0, void 0, function* () {
                            graphqlUnknownOperationName =
                                requestContext.operation === undefined;
                            yield shouldIncludeRequest(requestContext);
                            if (metrics.captureTraces === false) {
                                didEnd(requestContext);
                            }
                        });
                    },
                    executionDidStart() {
                        if (endDone)
                            return;
                        return {
                            willResolveField({ info }) {
                                return treeBuilder.willResolveField(info);
                            },
                        };
                    },
                    willSendResponse(requestContext) {
                        didEnd(requestContext);
                    },
                    didEncounterErrors(requestContext) {
                        return __awaiter(this, void 0, void 0, function* () {
                            if (!didResolveSource || endDone)
                                return;
                            treeBuilder.didEncounterErrors(requestContext.errors);
                            yield shouldIncludeRequest(requestContext);
                            didEnd(requestContext);
                        });
                    },
                };
            };
            return {
                serverWillStop() {
                    return __awaiter(this, void 0, void 0, function* () {
                        if (reportTimer) {
                            clearInterval(reportTimer);
                            reportTimer = undefined;
                        }
                        stopped = true;
                        yield sendAllReportsAndReportErrors();
                    });
                },
            };
        },
    };
}
exports.ApolloServerPluginUsageReporting = ApolloServerPluginUsageReporting;
function makeHTTPRequestHeaders(http, headers, sendHeaders) {
    if (!sendHeaders ||
        ('none' in sendHeaders && sendHeaders.none) ||
        ('all' in sendHeaders && !sendHeaders.all)) {
        return;
    }
    for (const [key, value] of headers) {
        const lowerCaseKey = key.toLowerCase();
        if (('exceptNames' in sendHeaders &&
            sendHeaders.exceptNames.some((exceptHeader) => {
                return exceptHeader.toLowerCase() === lowerCaseKey;
            })) ||
            ('onlyNames' in sendHeaders &&
                !sendHeaders.onlyNames.some((header) => {
                    return header.toLowerCase() === lowerCaseKey;
                }))) {
            continue;
        }
        switch (key) {
            case 'authorization':
            case 'cookie':
            case 'set-cookie':
                break;
            default:
                http.requestHeaders[key] = new apollo_reporting_protobuf_1.Trace.HTTP.Values({
                    value: [value],
                });
        }
    }
}
exports.makeHTTPRequestHeaders = makeHTTPRequestHeaders;
function defaultGenerateClientInfo({ request }) {
    var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
    const clientNameHeaderKey = 'apollographql-client-name';
    const clientReferenceIdHeaderKey = 'apollographql-client-reference-id';
    const clientVersionHeaderKey = 'apollographql-client-version';
    if (((_b = (_a = request.http) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b.get(clientNameHeaderKey)) || ((_d = (_c = request.http) === null || _c === void 0 ? void 0 : _c.headers) === null || _d === void 0 ? void 0 : _d.get(clientVersionHeaderKey)) || ((_f = (_e = request.http) === null || _e === void 0 ? void 0 : _e.headers) === null || _f === void 0 ? void 0 : _f.get(clientReferenceIdHeaderKey))) {
        return {
            clientName: (_h = (_g = request.http) === null || _g === void 0 ? void 0 : _g.headers) === null || _h === void 0 ? void 0 : _h.get(clientNameHeaderKey),
            clientVersion: (_k = (_j = request.http) === null || _j === void 0 ? void 0 : _j.headers) === null || _k === void 0 ? void 0 : _k.get(clientVersionHeaderKey),
            clientReferenceId: (_m = (_l = request.http) === null || _l === void 0 ? void 0 : _l.headers) === null || _m === void 0 ? void 0 : _m.get(clientReferenceIdHeaderKey),
        };
    }
    else if ((_o = request.extensions) === null || _o === void 0 ? void 0 : _o.clientInfo) {
        return request.extensions.clientInfo;
    }
    else {
        return {};
    }
}
function ApolloServerPluginUsageReportingDisabled() {
    return {
        __internal_plugin_id__() {
            return 'UsageReporting';
        },
    };
}
exports.ApolloServerPluginUsageReportingDisabled = ApolloServerPluginUsageReportingDisabled;
//# sourceMappingURL=plugin.js.map