UNPKG

@sentry/browser

Version:
145 lines (142 loc) 5.94 kB
import { defineIntegration, spanToJSON, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_URL_FULL, SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD, isString, stringMatchesSomePattern } from '@sentry/core/browser'; import { SENTRY_XHR_DATA_KEY, getBodyString, getFetchRequestArgBody } from '@sentry-internal/browser-utils'; const INTEGRATION_NAME = "GraphQLClient"; const _graphqlClientIntegration = ((options) => { return { name: INTEGRATION_NAME, setup(client) { _updateSpanWithGraphQLData(client, options); _updateBreadcrumbWithGraphQLData(client, options); } }; }); function _updateSpanWithGraphQLData(client, options) { client.on("beforeOutgoingRequestSpan", (span, hint) => { const spanJSON = spanToJSON(span); const spanAttributes = spanJSON.data || {}; const spanOp = spanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_OP]; const isHttpClientSpan = spanOp === "http.client"; if (!isHttpClientSpan) { return; } const httpUrl = spanAttributes[SEMANTIC_ATTRIBUTE_URL_FULL] || spanAttributes["http.url"] || spanAttributes["url"]; const httpMethod = spanAttributes[SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD] || spanAttributes["http.method"]; if (!isString(httpUrl) || !isString(httpMethod)) { return; } const { endpoints } = options; const isTracedGraphqlEndpoint = stringMatchesSomePattern(httpUrl, endpoints); const payload = getRequestPayloadXhrOrFetch(hint); if (isTracedGraphqlEndpoint && payload) { const graphqlBody = getGraphQLRequestPayload(payload); if (graphqlBody) { const operationInfo = _getGraphQLOperation(graphqlBody); span.updateName(`${httpMethod} ${httpUrl} (${operationInfo})`); if (isStandardRequest(graphqlBody)) { span.setAttribute("graphql.document", graphqlBody.query); } if (isPersistedRequest(graphqlBody)) { span.setAttribute("graphql.persisted_query.hash.sha256", graphqlBody.extensions.persistedQuery.sha256Hash); span.setAttribute("graphql.persisted_query.version", graphqlBody.extensions.persistedQuery.version); } } } }); } function _updateBreadcrumbWithGraphQLData(client, options) { client.on("beforeOutgoingRequestBreadcrumb", (breadcrumb, handlerData) => { const { category, type, data } = breadcrumb; const isFetch = category === "fetch"; const isXhr = category === "xhr"; const isHttpBreadcrumb = type === "http"; if (isHttpBreadcrumb && (isFetch || isXhr)) { const httpUrl = data?.url; const { endpoints } = options; const isTracedGraphqlEndpoint = stringMatchesSomePattern(httpUrl, endpoints); const payload = getRequestPayloadXhrOrFetch(handlerData); if (isTracedGraphqlEndpoint && data && payload) { const graphqlBody = getGraphQLRequestPayload(payload); if (!data.graphql && graphqlBody) { const operationInfo = _getGraphQLOperation(graphqlBody); data["graphql.operation"] = operationInfo; if (isStandardRequest(graphqlBody)) { data["graphql.document"] = graphqlBody.query; } if (isPersistedRequest(graphqlBody)) { data["graphql.persisted_query.hash.sha256"] = graphqlBody.extensions.persistedQuery.sha256Hash; data["graphql.persisted_query.version"] = graphqlBody.extensions.persistedQuery.version; } } } } }); } function _getGraphQLOperation(requestBody) { if (isPersistedRequest(requestBody)) { return `persisted ${requestBody.operationName}`; } if (isStandardRequest(requestBody)) { const { query: graphqlQuery, operationName: graphqlOperationName } = requestBody; const { operationName = graphqlOperationName, operationType } = parseGraphQLQuery(graphqlQuery); const operationInfo = operationName ? `${operationType} ${operationName}` : `${operationType}`; return operationInfo; } return "unknown"; } function getRequestPayloadXhrOrFetch(hint) { const isXhr = "xhr" in hint; let body; if (isXhr) { const sentryXhrData = hint.xhr[SENTRY_XHR_DATA_KEY]; body = sentryXhrData && getBodyString(sentryXhrData.body)[0]; } else { const sentryFetchData = getFetchRequestArgBody(hint.input); body = getBodyString(sentryFetchData)[0]; } return body; } function parseGraphQLQuery(query) { const namedQueryRe = /^(?:\s*)(query|mutation|subscription)(?:\s*)(\w+)(?:\s*)[{(]/; const unnamedQueryRe = /^(?:\s*)(query|mutation|subscription)(?:\s*)[{(]/; const namedMatch = query.match(namedQueryRe); if (namedMatch) { return { operationType: namedMatch[1], operationName: namedMatch[2] }; } const unnamedMatch = query.match(unnamedQueryRe); if (unnamedMatch) { return { operationType: unnamedMatch[1], operationName: void 0 }; } return { operationType: void 0, operationName: void 0 }; } function isObject(value) { return typeof value === "object" && value !== null; } function isStandardRequest(payload) { return isObject(payload) && typeof payload.query === "string"; } function isPersistedRequest(payload) { return isObject(payload) && typeof payload.operationName === "string" && isObject(payload.extensions) && isObject(payload.extensions.persistedQuery) && typeof payload.extensions.persistedQuery.sha256Hash === "string" && typeof payload.extensions.persistedQuery.version === "number"; } function getGraphQLRequestPayload(payload) { try { const requestBody = JSON.parse(payload); if (isStandardRequest(requestBody) || isPersistedRequest(requestBody)) { return requestBody; } return void 0; } catch { return void 0; } } const graphqlClientIntegration = defineIntegration(_graphqlClientIntegration); export { _getGraphQLOperation, getGraphQLRequestPayload, getRequestPayloadXhrOrFetch, graphqlClientIntegration, parseGraphQLQuery }; //# sourceMappingURL=graphqlClient.js.map