@multiplayer-app/session-recorder-browser
Version:
Multiplayer Fullstack Session Recorder for Browser
208 lines • 8.45 kB
JavaScript
import { MULTIPLAYER_TRACE_DEBUG_PREFIX, MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX, ATTR_MULTIPLAYER_HTTP_REQUEST_BODY, ATTR_MULTIPLAYER_HTTP_REQUEST_HEADERS, ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY, ATTR_MULTIPLAYER_HTTP_RESPONSE_HEADERS, } from '@multiplayer-app/session-recorder-common';
/**
* Checks if the trace should be processed based on trace ID prefixes
*/
export function shouldProcessTrace(traceId) {
return (traceId.startsWith(MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
traceId.startsWith(MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX));
}
/**
* Processes request and response body based on trace type and configuration
*/
export function processBody(payload, config, span) {
var _a, _b;
const { captureBody, masking } = config;
const traceId = span.spanContext().traceId;
if (!captureBody) {
return {};
}
let { requestBody, responseBody } = payload;
if (requestBody !== undefined && requestBody !== null) {
requestBody = JSON.parse(JSON.stringify(requestBody));
}
if (responseBody !== undefined && responseBody !== null) {
responseBody = JSON.parse(JSON.stringify(responseBody));
}
// Apply masking for debug traces
if (traceId.startsWith(MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
traceId.startsWith(MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)) {
if (masking.isContentMaskingEnabled) {
requestBody = requestBody && ((_a = masking.maskBody) === null || _a === void 0 ? void 0 : _a.call(masking, requestBody, span));
responseBody = responseBody && ((_b = masking.maskBody) === null || _b === void 0 ? void 0 : _b.call(masking, responseBody, span));
}
}
// Convert to string if needed
if (typeof requestBody !== 'string') {
requestBody = JSON.stringify(requestBody);
}
if (typeof responseBody !== 'string') {
responseBody = JSON.stringify(responseBody);
}
return {
requestBody: (requestBody === null || requestBody === void 0 ? void 0 : requestBody.length) ? requestBody : undefined,
responseBody: (responseBody === null || responseBody === void 0 ? void 0 : responseBody.length) ? responseBody : undefined,
};
}
/**
* Processes request and response headers based on configuration
*/
export function processHeaders(payload, config, span) {
var _a, _b, _c, _d, _e;
const { captureHeaders, masking } = config;
if (!captureHeaders) {
return {};
}
let { requestHeaders = {}, responseHeaders = {} } = payload;
// Handle header filtering
if (!((_a = masking.headersToInclude) === null || _a === void 0 ? void 0 : _a.length) &&
!((_b = masking.headersToExclude) === null || _b === void 0 ? void 0 : _b.length)) {
// Add null checks to prevent JSON.parse error when headers is undefined
if (requestHeaders !== undefined && requestHeaders !== null) {
requestHeaders = JSON.parse(JSON.stringify(requestHeaders));
}
if (responseHeaders !== undefined && responseHeaders !== null) {
responseHeaders = JSON.parse(JSON.stringify(responseHeaders));
}
}
else {
if (masking.headersToInclude) {
const _requestHeaders = {};
const _responseHeaders = {};
for (const headerName of masking.headersToInclude) {
if (requestHeaders[headerName]) {
_requestHeaders[headerName] = requestHeaders[headerName];
}
if (responseHeaders[headerName]) {
_responseHeaders[headerName] = responseHeaders[headerName];
}
}
requestHeaders = _requestHeaders;
responseHeaders = _responseHeaders;
}
if ((_c = masking.headersToExclude) === null || _c === void 0 ? void 0 : _c.length) {
for (const headerName of masking.headersToExclude) {
delete requestHeaders[headerName];
delete responseHeaders[headerName];
}
}
}
// Apply masking
const maskedRequestHeaders = ((_d = masking.maskHeaders) === null || _d === void 0 ? void 0 : _d.call(masking, requestHeaders, span)) || requestHeaders;
const maskedResponseHeaders = ((_e = masking.maskHeaders) === null || _e === void 0 ? void 0 : _e.call(masking, responseHeaders, span)) || responseHeaders;
// Convert to string
const requestHeadersStr = typeof maskedRequestHeaders === 'string'
? maskedRequestHeaders
: JSON.stringify(maskedRequestHeaders);
const responseHeadersStr = typeof maskedResponseHeaders === 'string'
? maskedResponseHeaders
: JSON.stringify(maskedResponseHeaders);
return {
requestHeaders: (requestHeadersStr === null || requestHeadersStr === void 0 ? void 0 : requestHeadersStr.length) ? requestHeadersStr : undefined,
responseHeaders: (responseHeadersStr === null || responseHeadersStr === void 0 ? void 0 : responseHeadersStr.length) ? responseHeadersStr : undefined,
};
}
/**
* Processes HTTP payload (body and headers) and sets span attributes
*/
export function processHttpPayload(payload, config, span) {
const traceId = span.spanContext().traceId;
if (!shouldProcessTrace(traceId)) {
return;
}
const { requestBody, responseBody } = processBody(payload, config, span);
const { requestHeaders, responseHeaders } = processHeaders(payload, config, span);
// Set span attributes
if (requestBody) {
span.setAttribute(ATTR_MULTIPLAYER_HTTP_REQUEST_BODY, requestBody);
}
if (responseBody) {
span.setAttribute(ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY, responseBody);
}
if (requestHeaders) {
span.setAttribute(ATTR_MULTIPLAYER_HTTP_REQUEST_HEADERS, requestHeaders);
}
if (responseHeaders) {
span.setAttribute(ATTR_MULTIPLAYER_HTTP_RESPONSE_HEADERS, responseHeaders);
}
}
/**
* Converts Headers object to plain object
*/
export function headersToObject(headers) {
const result = {};
if (!headers) {
return result;
}
if (headers instanceof Headers) {
headers.forEach((value, key) => {
result[key] = value;
});
}
else if (Array.isArray(headers)) {
// Handle array of [key, value] pairs
for (const [key, value] of headers) {
if (typeof key === 'string' && typeof value === 'string') {
result[key] = value;
}
}
}
else if (typeof headers === 'object' && !Array.isArray(headers)) {
for (const [key, value] of Object.entries(headers)) {
if (typeof key === 'string' && typeof value === 'string') {
result[key] = value;
}
}
}
return result;
}
/**
* Extracts response body as string from Response object
*/
export async function extractResponseBody(response) {
if (!response.body) {
return null;
}
try {
if (response.body instanceof ReadableStream) {
// Check if response body is already consumed
if (response.bodyUsed) {
return null;
}
const responseClone = response.clone();
return responseClone.text();
}
else {
return JSON.stringify(response.body);
}
}
catch (error) {
// If cloning fails (body already consumed), return null
// eslint-disable-next-line no-console
console.warn('[MULTIPLAYER_SESSION_RECORDER] Failed to extract response body:', error);
return null;
}
}
export const getExporterEndpoint = (exporterEndpoint) => {
const hasPath = exporterEndpoint && (() => {
try {
const url = new URL(exporterEndpoint);
return url.pathname !== '/' && url.pathname !== '';
}
catch (_a) {
return false;
}
})();
if (hasPath) {
return exporterEndpoint;
}
const trimmedExporterEndpoint = new URL(exporterEndpoint).origin;
return `${trimmedExporterEndpoint}/v1/traces`;
};
export const getElementInnerText = (element) => {
return String(element.innerText || '').trim();
};
export const getElementTextContent = (element) => {
var _a;
return String(((_a = element.textContent) === null || _a === void 0 ? void 0 : _a.split('\n').filter(Boolean).join(' ')) || '').trim();
};
//# sourceMappingURL=helpers.js.map