From 100369e6a52ecf435709ab70bf53d1018ce2d793 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 20 Apr 2023 11:27:07 +0200 Subject: [PATCH] feat(replay): Truncate network bodies to max size (#7875) --------- Co-authored-by: Abhijeet Prasad --- packages/replay/src/constants.ts | 4 +- .../src/coreHandlers/util/fetchUtils.ts | 29 +- .../src/coreHandlers/util/networkUtils.ts | 55 +++- .../replay/src/coreHandlers/util/xhrUtils.ts | 5 +- packages/replay/src/types.ts | 9 +- .../src/util/truncateJson/completeJson.ts | 125 +++++++++ .../replay/src/util/truncateJson/constants.ts | 23 ++ .../src/util/truncateJson/evaluateJson.ts | 264 ++++++++++++++++++ .../replay/src/util/truncateJson/fixJson.ts | 14 + .../test/fixtures/fixJson/1_completeJson.json | 1 + .../fixtures/fixJson/1_incompleteJson.txt | 1 + .../test/fixtures/fixJson/2_completeJson.json | 22 ++ .../fixtures/fixJson/2_incompleteJson.txt | 22 ++ .../handleNetworkBreadcrumbs.test.ts | 173 +++++++++++- .../coreHandlers/util/networkUtils.test.ts | 127 ++++++++- .../util/truncateJson/fixJson.test.ts | 82 ++++++ 16 files changed, 914 insertions(+), 42 deletions(-) create mode 100644 packages/replay/src/util/truncateJson/completeJson.ts create mode 100644 packages/replay/src/util/truncateJson/constants.ts create mode 100644 packages/replay/src/util/truncateJson/evaluateJson.ts create mode 100644 packages/replay/src/util/truncateJson/fixJson.ts create mode 100644 packages/replay/test/fixtures/fixJson/1_completeJson.json create mode 100644 packages/replay/test/fixtures/fixJson/1_incompleteJson.txt create mode 100644 packages/replay/test/fixtures/fixJson/2_completeJson.json create mode 100644 packages/replay/test/fixtures/fixJson/2_incompleteJson.txt create mode 100644 packages/replay/test/unit/coreHandlers/util/truncateJson/fixJson.test.ts diff --git a/packages/replay/src/constants.ts b/packages/replay/src/constants.ts index 57c30fdad662..68ff5fc481ff 100644 --- a/packages/replay/src/constants.ts +++ b/packages/replay/src/constants.ts @@ -29,5 +29,5 @@ export const ERROR_CHECKOUT_TIME = 60_000; export const RETRY_BASE_INTERVAL = 5000; export const RETRY_MAX_COUNT = 3; -/* The max (uncompressed) size in bytes of a network body. Any body larger than this will be dropped. */ -export const NETWORK_BODY_MAX_SIZE = 300_000; +/* The max (uncompressed) size in bytes of a network body. Any body larger than this will be truncated. */ +export const NETWORK_BODY_MAX_SIZE = 150_000; diff --git a/packages/replay/src/coreHandlers/util/fetchUtils.ts b/packages/replay/src/coreHandlers/util/fetchUtils.ts index 1946efd44f07..b2f7a1ac72d7 100644 --- a/packages/replay/src/coreHandlers/util/fetchUtils.ts +++ b/packages/replay/src/coreHandlers/util/fetchUtils.ts @@ -3,7 +3,6 @@ import { logger } from '@sentry/utils'; import type { FetchHint, - NetworkBody, ReplayContainer, ReplayNetworkOptions, ReplayNetworkRequestData, @@ -15,7 +14,6 @@ import { getAllowedHeaders, getBodySize, getBodyString, - getNetworkBody, makeNetworkReplayBreadcrumb, parseContentLengthHeader, } from './networkUtils'; @@ -112,8 +110,8 @@ function _getRequestInfo( // We only want to transmit string or string-like bodies const requestBody = _getFetchRequestArgBody(input); - const body = getNetworkBody(getBodyString(requestBody)); - return buildNetworkRequestOrResponse(headers, requestBodySize, body); + const bodyStr = getBodyString(requestBody); + return buildNetworkRequestOrResponse(headers, requestBodySize, bodyStr); } async function _getResponseInfo( @@ -137,7 +135,7 @@ async function _getResponseInfo( try { // We have to clone this, as the body can only be read once const res = response.clone(); - const { body, bodyText } = await _parseFetchBody(res); + const bodyText = await _parseFetchBody(res); const size = bodyText && bodyText.length && responseBodySize === undefined @@ -145,7 +143,7 @@ async function _getResponseInfo( : responseBodySize; if (captureBodies) { - return buildNetworkRequestOrResponse(headers, size, body); + return buildNetworkRequestOrResponse(headers, size, bodyText); } return buildNetworkRequestOrResponse(headers, size, undefined); @@ -155,25 +153,12 @@ async function _getResponseInfo( } } -async function _parseFetchBody( - response: Response, -): Promise<{ body?: NetworkBody | undefined; bodyText?: string | undefined }> { - let bodyText: string; - +async function _parseFetchBody(response: Response): Promise { try { - bodyText = await response.text(); + return await response.text(); } catch { - return {}; - } - - try { - const body = JSON.parse(bodyText); - return { body, bodyText }; - } catch { - // just send bodyText + return undefined; } - - return { bodyText, body: bodyText }; } function _getFetchRequestArgBody(fetchArgs: unknown[] = []): RequestInit['body'] | undefined { diff --git a/packages/replay/src/coreHandlers/util/networkUtils.ts b/packages/replay/src/coreHandlers/util/networkUtils.ts index 498fb0960c30..6ff4a6ce27d9 100644 --- a/packages/replay/src/coreHandlers/util/networkUtils.ts +++ b/packages/replay/src/coreHandlers/util/networkUtils.ts @@ -4,11 +4,13 @@ import { dropUndefinedKeys } from '@sentry/utils'; import { NETWORK_BODY_MAX_SIZE } from '../../constants'; import type { NetworkBody, + NetworkMetaWarning, NetworkRequestData, ReplayNetworkRequestData, ReplayNetworkRequestOrResponse, ReplayPerformanceEntry, } from '../../types'; +import { fixJson } from '../../util/truncateJson/fixJson'; /** Get the size of a body. */ export function getBodySize( @@ -122,7 +124,7 @@ export function getNetworkBody(bodyText: string | undefined): NetworkBody | unde export function buildNetworkRequestOrResponse( headers: Record, bodySize: number | undefined, - body: NetworkBody | undefined, + body: string | undefined, ): ReplayNetworkRequestOrResponse | undefined { if (!bodySize && Object.keys(headers).length === 0) { return undefined; @@ -146,11 +148,11 @@ export function buildNetworkRequestOrResponse( size: bodySize, }; - if (bodySize < NETWORK_BODY_MAX_SIZE) { - info.body = body; - } else { + const { body: normalizedBody, warnings } = normalizeNetworkBody(body); + info.body = normalizedBody; + if (warnings.length > 0) { info._meta = { - errors: ['MAX_BODY_SIZE_EXCEEDED'], + warnings, }; } @@ -175,3 +177,46 @@ function _serializeFormData(formData: FormData): string { // @ts-ignore passing FormData to URLSearchParams actually works return new URLSearchParams(formData).toString(); } + +function normalizeNetworkBody(body: string | undefined): { + body: NetworkBody | undefined; + warnings: NetworkMetaWarning[]; +} { + if (!body || typeof body !== 'string') { + return { + body, + warnings: [], + }; + } + + const exceedsSizeLimit = body.length > NETWORK_BODY_MAX_SIZE; + + if (_strIsProbablyJson(body)) { + try { + const json = exceedsSizeLimit ? fixJson(body.slice(0, NETWORK_BODY_MAX_SIZE)) : body; + const normalizedBody = JSON.parse(json); + return { + body: normalizedBody, + warnings: exceedsSizeLimit ? ['JSON_TRUNCATED'] : [], + }; + } catch { + return { + body, + warnings: ['INVALID_JSON'], + }; + } + } + + return { + body: exceedsSizeLimit ? `${body.slice(0, NETWORK_BODY_MAX_SIZE)}…` : body, + warnings: exceedsSizeLimit ? ['TEXT_TRUNCATED'] : [], + }; +} + +function _strIsProbablyJson(str: string): boolean { + const first = str[0]; + const last = str[str.length - 1]; + + // Simple check: If this does not start & end with {} or [], it's not JSON + return (first === '[' && last === ']') || (first === '{' && last === '}'); +} diff --git a/packages/replay/src/coreHandlers/util/xhrUtils.ts b/packages/replay/src/coreHandlers/util/xhrUtils.ts index b39148592476..b241bd945771 100644 --- a/packages/replay/src/coreHandlers/util/xhrUtils.ts +++ b/packages/replay/src/coreHandlers/util/xhrUtils.ts @@ -8,7 +8,6 @@ import { getAllowedHeaders, getBodySize, getBodyString, - getNetworkBody, makeNetworkReplayBreadcrumb, parseContentLengthHeader, } from './networkUtils'; @@ -84,12 +83,12 @@ function _prepareXhrData( const request = buildNetworkRequestOrResponse( requestHeaders, requestBodySize, - options.captureBodies ? getNetworkBody(getBodyString(input)) : undefined, + options.captureBodies ? getBodyString(input) : undefined, ); const response = buildNetworkRequestOrResponse( responseHeaders, responseBodySize, - options.captureBodies ? getNetworkBody(hint.xhr.responseText) : undefined, + options.captureBodies ? hint.xhr.responseText : undefined, ); return { diff --git a/packages/replay/src/types.ts b/packages/replay/src/types.ts index 98b7ebc4063a..e9faa29b9d6d 100644 --- a/packages/replay/src/types.ts +++ b/packages/replay/src/types.ts @@ -512,12 +512,15 @@ export type FetchHint = FetchBreadcrumbHint & { response: Response; }; -export type NetworkBody = Record | string; +type JsonObject = Record; +type JsonArray = unknown[]; -type NetworkMetaError = 'MAX_BODY_SIZE_EXCEEDED'; +export type NetworkBody = JsonObject | JsonArray | string; + +export type NetworkMetaWarning = 'JSON_TRUNCATED' | 'TEXT_TRUNCATED' | 'INVALID_JSON'; interface NetworkMeta { - errors?: NetworkMetaError[]; + warnings?: NetworkMetaWarning[]; } export interface ReplayNetworkRequestOrResponse { diff --git a/packages/replay/src/util/truncateJson/completeJson.ts b/packages/replay/src/util/truncateJson/completeJson.ts new file mode 100644 index 000000000000..3e7be2f38a13 --- /dev/null +++ b/packages/replay/src/util/truncateJson/completeJson.ts @@ -0,0 +1,125 @@ +import type { JsonToken } from './constants'; +import { + ARR, + ARR_VAL, + ARR_VAL_COMPLETED, + ARR_VAL_STR, + OBJ, + OBJ_KEY, + OBJ_KEY_STR, + OBJ_VAL, + OBJ_VAL_COMPLETED, + OBJ_VAL_STR, +} from './constants'; + +const ALLOWED_PRIMITIVES = ['true', 'false', 'null']; + +/** + * Complete an incomplete JSON string. + * This will ensure that the last element always has a `"~~"` to indicate it was truncated. + * For example, `[1,2,` will be completed to `[1,2,"~~"]` + * and `{"aa":"b` will be completed to `{"aa":"b~~"}` + */ +export function completeJson(incompleteJson: string, stack: JsonToken[]): string { + if (!stack.length) { + return incompleteJson; + } + + let json = incompleteJson; + + // Most checks are only needed for the last step in the stack + const lastPos = stack.length - 1; + const lastStep = stack[lastPos]; + + json = _fixLastStep(json, lastStep); + + // Complete remaining steps - just add closing brackets + for (let i = lastPos; i >= 0; i--) { + const step = stack[i]; + + switch (step) { + case OBJ: + json = `${json}}`; + break; + case ARR: + json = `${json}]`; + break; + } + } + + return json; +} + +function _fixLastStep(json: string, lastStep: JsonToken): string { + switch (lastStep) { + // Object cases + case OBJ: + return `${json}"~~":"~~"`; + case OBJ_KEY: + return `${json}:"~~"`; + case OBJ_KEY_STR: + return `${json}~~":"~~"`; + case OBJ_VAL: + return _maybeFixIncompleteObjValue(json); + case OBJ_VAL_STR: + return `${json}~~"`; + case OBJ_VAL_COMPLETED: + return `${json},"~~":"~~"`; + + // Array cases + case ARR: + return `${json}"~~"`; + case ARR_VAL: + return _maybeFixIncompleteArrValue(json); + case ARR_VAL_STR: + return `${json}~~"`; + case ARR_VAL_COMPLETED: + return `${json},"~~"`; + } + + return json; +} + +function _maybeFixIncompleteArrValue(json: string): string { + const pos = _findLastArrayDelimiter(json); + + if (pos > -1) { + const part = json.slice(pos + 1); + + if (ALLOWED_PRIMITIVES.includes(part.trim())) { + return `${json},"~~"`; + } + + // Everything else is replaced with `"~~"` + return `${json.slice(0, pos + 1)}"~~"`; + } + + // fallback, this shouldn't happen, to be save + return json; +} + +function _findLastArrayDelimiter(json: string): number { + for (let i = json.length - 1; i >= 0; i--) { + const char = json[i]; + + if (char === ',' || char === '[') { + return i; + } + } + + return -1; +} + +function _maybeFixIncompleteObjValue(json: string): string { + const startPos = json.lastIndexOf(':'); + + const part = json.slice(startPos + 1); + + if (ALLOWED_PRIMITIVES.includes(part.trim())) { + return `${json},"~~":"~~"`; + } + + // Everything else is replaced with `"~~"` + // This also means we do not have incomplete numbers, e.g `[1` is replaced with `["~~"]` + return `${json.slice(0, startPos + 1)}"~~"`; +} diff --git a/packages/replay/src/util/truncateJson/constants.ts b/packages/replay/src/util/truncateJson/constants.ts new file mode 100644 index 000000000000..6ea4f2dda3e2 --- /dev/null +++ b/packages/replay/src/util/truncateJson/constants.ts @@ -0,0 +1,23 @@ +export const OBJ = 10; +export const OBJ_KEY = 11; +export const OBJ_KEY_STR = 12; +export const OBJ_VAL = 13; +export const OBJ_VAL_STR = 14; +export const OBJ_VAL_COMPLETED = 15; + +export const ARR = 20; +export const ARR_VAL = 21; +export const ARR_VAL_STR = 22; +export const ARR_VAL_COMPLETED = 23; + +export type JsonToken = + | typeof OBJ + | typeof OBJ_KEY + | typeof OBJ_KEY_STR + | typeof OBJ_VAL + | typeof OBJ_VAL_STR + | typeof OBJ_VAL_COMPLETED + | typeof ARR + | typeof ARR_VAL + | typeof ARR_VAL_STR + | typeof ARR_VAL_COMPLETED; diff --git a/packages/replay/src/util/truncateJson/evaluateJson.ts b/packages/replay/src/util/truncateJson/evaluateJson.ts new file mode 100644 index 000000000000..0ba8d79c4c9a --- /dev/null +++ b/packages/replay/src/util/truncateJson/evaluateJson.ts @@ -0,0 +1,264 @@ +import type { JsonToken } from './constants'; +import { + ARR, + ARR_VAL, + ARR_VAL_COMPLETED, + ARR_VAL_STR, + OBJ, + OBJ_KEY, + OBJ_KEY_STR, + OBJ_VAL, + OBJ_VAL_COMPLETED, + OBJ_VAL_STR, +} from './constants'; + +/** + * Evaluate an (incomplete) JSON string. + */ +export function evaluateJson(json: string): JsonToken[] { + const stack: JsonToken[] = []; + + for (let pos = 0; pos < json.length; pos++) { + _evaluateJsonPos(stack, json, pos); + } + + return stack; +} + +function _evaluateJsonPos(stack: JsonToken[], json: string, pos: number): void { + const curStep = stack[stack.length - 1]; + + const char = json[pos]; + + const whitespaceRegex = /\s/; + + if (whitespaceRegex.test(char)) { + return; + } + + if (char === '"' && !_isEscaped(json, pos)) { + _handleQuote(stack, curStep); + return; + } + + switch (char) { + case '{': + _handleObj(stack, curStep); + break; + case '[': + _handleArr(stack, curStep); + break; + case ':': + _handleColon(stack, curStep); + break; + case ',': + _handleComma(stack, curStep); + break; + case '}': + _handleObjClose(stack, curStep); + break; + case ']': + _handleArrClose(stack, curStep); + break; + } +} + +function _handleQuote(stack: JsonToken[], curStep: JsonToken): void { + // End of obj value + if (curStep === OBJ_VAL_STR) { + stack.pop(); + stack.push(OBJ_VAL_COMPLETED); + return; + } + + // End of arr value + if (curStep === ARR_VAL_STR) { + stack.pop(); + stack.push(ARR_VAL_COMPLETED); + return; + } + + // Start of obj value + if (curStep === OBJ_VAL) { + stack.push(OBJ_VAL_STR); + return; + } + + // Start of arr value + if (curStep === ARR_VAL) { + stack.push(ARR_VAL_STR); + return; + } + + // Start of obj key + if (curStep === OBJ) { + stack.push(OBJ_KEY_STR); + return; + } + + // End of obj key + if (curStep === OBJ_KEY_STR) { + stack.pop(); + stack.push(OBJ_KEY); + return; + } +} + +function _handleObj(stack: JsonToken[], curStep: JsonToken): void { + // Initial object + if (!curStep) { + stack.push(OBJ); + return; + } + + // New object as obj value + if (curStep === OBJ_VAL) { + stack.push(OBJ); + return; + } + + // New object as array element + if (curStep === ARR_VAL) { + stack.push(OBJ); + } + + // New object as first array element + if (curStep === ARR) { + stack.push(OBJ); + return; + } +} + +function _handleArr(stack: JsonToken[], curStep: JsonToken): void { + // Initial array + if (!curStep) { + stack.push(ARR); + stack.push(ARR_VAL); + return; + } + + // New array as obj value + if (curStep === OBJ_VAL) { + stack.push(ARR); + stack.push(ARR_VAL); + return; + } + + // New array as array element + if (curStep === ARR_VAL) { + stack.push(ARR); + stack.push(ARR_VAL); + } + + // New array as first array element + if (curStep === ARR) { + stack.push(ARR); + stack.push(ARR_VAL); + return; + } +} + +function _handleColon(stack: JsonToken[], curStep: JsonToken): void { + if (curStep === OBJ_KEY) { + stack.pop(); + stack.push(OBJ_VAL); + } +} + +function _handleComma(stack: JsonToken[], curStep: JsonToken): void { + // Comma after obj value + if (curStep === OBJ_VAL) { + stack.pop(); + return; + } + if (curStep === OBJ_VAL_COMPLETED) { + // Pop OBJ_VAL_COMPLETED & OBJ_VAL + stack.pop(); + stack.pop(); + return; + } + + // Comma after arr value + if (curStep === ARR_VAL) { + // do nothing - basically we'd pop ARR_VAL but add it right back + return; + } + + if (curStep === ARR_VAL_COMPLETED) { + // Pop ARR_VAL_COMPLETED + stack.pop(); + + // basically we'd pop ARR_VAL but add it right back + return; + } +} + +function _handleObjClose(stack: JsonToken[], curStep: JsonToken): void { + // Empty object {} + if (curStep === OBJ) { + stack.pop(); + } + + // Object with element + if (curStep === OBJ_VAL) { + // Pop OBJ_VAL, OBJ + stack.pop(); + stack.pop(); + } + + // Obj with element + if (curStep === OBJ_VAL_COMPLETED) { + // Pop OBJ_VAL_COMPLETED, OBJ_VAL, OBJ + stack.pop(); + stack.pop(); + stack.pop(); + } + + // if was obj value, complete it + if (stack[stack.length - 1] === OBJ_VAL) { + stack.push(OBJ_VAL_COMPLETED); + } + + // if was arr value, complete it + if (stack[stack.length - 1] === ARR_VAL) { + stack.push(ARR_VAL_COMPLETED); + } +} + +function _handleArrClose(stack: JsonToken[], curStep: JsonToken): void { + // Empty array [] + if (curStep === ARR) { + stack.pop(); + } + + // Array with element + if (curStep === ARR_VAL) { + // Pop ARR_VAL, ARR + stack.pop(); + stack.pop(); + } + + // Array with element + if (curStep === ARR_VAL_COMPLETED) { + // Pop ARR_VAL_COMPLETED, ARR_VAL, ARR + stack.pop(); + stack.pop(); + stack.pop(); + } + + // if was obj value, complete it + if (stack[stack.length - 1] === OBJ_VAL) { + stack.push(OBJ_VAL_COMPLETED); + } + + // if was arr value, complete it + if (stack[stack.length - 1] === ARR_VAL) { + stack.push(ARR_VAL_COMPLETED); + } +} + +function _isEscaped(str: string, pos: number): boolean { + const previousChar = str[pos - 1]; + + return previousChar === '\\' && !_isEscaped(str, pos - 1); +} diff --git a/packages/replay/src/util/truncateJson/fixJson.ts b/packages/replay/src/util/truncateJson/fixJson.ts new file mode 100644 index 000000000000..b54d80f011c3 --- /dev/null +++ b/packages/replay/src/util/truncateJson/fixJson.ts @@ -0,0 +1,14 @@ +/* eslint-disable max-lines */ + +import { completeJson } from './completeJson'; +import { evaluateJson } from './evaluateJson'; + +/** + * Takes an incomplete JSON string, and returns a hopefully valid JSON string. + * Note that this _can_ fail, so you should check the return value is valid JSON. + */ +export function fixJson(incompleteJson: string): string { + const stack = evaluateJson(incompleteJson); + + return completeJson(incompleteJson, stack); +} diff --git a/packages/replay/test/fixtures/fixJson/1_completeJson.json b/packages/replay/test/fixtures/fixJson/1_completeJson.json new file mode 100644 index 000000000000..3865127e5a23 --- /dev/null +++ b/packages/replay/test/fixtures/fixJson/1_completeJson.json @@ -0,0 +1 @@ +[{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-04-28T20:30:27.149789Z","lastLogin":"2023-04-13T19:40:13.734339Z","has2fa":true,"lastActive":"2023-04-14T17:36:23.756369Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"manager","roleName":"Manager","orgRole":"manager","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-04-28T20:30:27.572480Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2021-08-24T13:30:01.719798Z","lastLogin":"2023-04-18T20:19:32.572869Z","has2fa":true,"lastActive":"2023-04-18T21:18:53.674493Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2021-08-24T13:30:02.620935Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-07-26T17:38:43.681246Z","lastLogin":"2023-04-18T20:54:31.433191Z","has2fa":true,"lastActive":"2023-04-18T21:11:15.027754Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"219db453b9ef4a47a6c071fc836752a8"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-07-26T17:38:44.185908Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sdks-publish","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2021-05-12T17:47:51.719114Z","lastLogin":"2022-12-20T23:26:24.301088Z","has2fa":true,"lastActive":"2023-01-03T23:40:16.202147Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":false,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2021-05-12T17:47:52.127039Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-08-08T16:44:23.558695Z","lastLogin":"2023-04-07T07:52:12.808207Z","has2fa":true,"lastActive":"2023-04-18T17:53:21.169085Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"4548fe09d6c949ecb027c70af1d1e8fd"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-08-10T11:13:06.618597Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sdks-publish","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2023-02-01T19:03:04.268258Z","lastLogin":"2023-04-11T17:41:59.073515Z","has2fa":true,"lastActive":"2023-04-17T23:10:34.614992Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2023-02-03T19:52:50.750648Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-09-19T20:21:02.168949Z","lastLogin":"2023-04-17T20:27:01.536887Z","has2fa":true,"lastActive":"2023-04-18T21:09:26.940838Z","isSuperuser":false,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"9e67f1e04e184833b8b3bfa45beea7ea"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2023-01-05T15:31:45.227526Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sentry","snuba"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2021-08-10T13:59:05.267005Z","lastLogin":"2023-04-18T17:28:59.989484Z","has2fa":true,"lastActive":"2023-04-18T17:28:50.801466Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":"5b2c870c491841d0bc114c9df5bb6d2e"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2021-08-10T13:59:06.023687Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2021-08-05T22:12:41.902738Z","lastLogin":"2023-04-18T21:02:34.963627Z","has2fa":true,"lastActive":"2023-04-18T21:17:28.257879Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-09-16T18:05:51.667528Z","inviteStatus":"approved","inviterName":"richard.ma@sentry.io","orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-10-14T23:15:17.528252Z","lastLogin":"2023-03-31T20:53:37.291706Z","has2fa":false,"lastActive":"2023-04-18T20:12:14.438701Z","isSuperuser":false,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-10-19T19:38:46.936422Z","inviteStatus":"approved","inviterName":"isabel.matwawana@sentry.io","orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-12-09T23:20:23.346432Z","lastLogin":"2022-12-09T23:20:23.765878Z","has2fa":false,"lastActive":"2022-12-23T02:01:40.052033Z","isSuperuser":false,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":false,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-12-09T23:20:23.745693Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2018-02-26T23:57:43.766558Z","lastLogin":"2023-01-19T19:11:45.061167Z","has2fa":true,"lastActive":"2023-01-19T19:11:31.930426Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":false,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2018-02-26T16:04:56.738643Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-03-07T14:05:53.423324Z","lastLogin":"2023-04-18T07:29:18.311034Z","has2fa":true,"lastActive":"2023-04-18T16:17:56.241831Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"1836fd44387a413a917cf052523623cc"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-03-07T14:05:54.238336Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sdks-publish","sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-02-07T14:25:08.448480Z","lastLogin":"2023-04-13T08:24:55.740046Z","has2fa":true,"lastActive":"2023-04-18T17:53:19.914958Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"835ade1ffa314f788b1e1015af13cacb"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-02-07T14:25:08.946588Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sdks-publish","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2017-11-15T09:07:16.014572Z","lastLogin":"2023-04-18T17:50:00.282464Z","has2fa":true,"lastActive":"2023-04-18T17:55:17.126066Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2017-11-15T09:07:16.036013Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance","snuba"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2019-02-08T21:51:46.074436Z","lastLogin":"2023-04-18T20:03:13.403607Z","has2fa":true,"lastActive":"2023-04-18T20:35:34.150539Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2019-02-08T21:51:46.795772Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-06-27T18:14:49.652095Z","lastLogin":"2023-04-18T18:26:07.379897Z","has2fa":true,"lastActive":"2023-04-18T21:22:08.385362Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"1fc882a55f7e43caad0069765a940d72"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-06-27T18:14:50.014771Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2019-10-01T12:26:22.368368Z","lastLogin":"2023-04-18T16:28:34.978170Z","has2fa":true,"lastActive":"2023-04-18T17:53:44.185207Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2019-10-01T12:26:22.971086Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sdks-publish","sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.p~~"}}] diff --git a/packages/replay/test/fixtures/fixJson/1_incompleteJson.txt b/packages/replay/test/fixtures/fixJson/1_incompleteJson.txt new file mode 100644 index 000000000000..4377e08e6c9d --- /dev/null +++ b/packages/replay/test/fixtures/fixJson/1_incompleteJson.txt @@ -0,0 +1 @@ +[{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-04-28T20:30:27.149789Z","lastLogin":"2023-04-13T19:40:13.734339Z","has2fa":true,"lastActive":"2023-04-14T17:36:23.756369Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"manager","roleName":"Manager","orgRole":"manager","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-04-28T20:30:27.572480Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2021-08-24T13:30:01.719798Z","lastLogin":"2023-04-18T20:19:32.572869Z","has2fa":true,"lastActive":"2023-04-18T21:18:53.674493Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2021-08-24T13:30:02.620935Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-07-26T17:38:43.681246Z","lastLogin":"2023-04-18T20:54:31.433191Z","has2fa":true,"lastActive":"2023-04-18T21:11:15.027754Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"219db453b9ef4a47a6c071fc836752a8"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-07-26T17:38:44.185908Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sdks-publish","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2021-05-12T17:47:51.719114Z","lastLogin":"2022-12-20T23:26:24.301088Z","has2fa":true,"lastActive":"2023-01-03T23:40:16.202147Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":false,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2021-05-12T17:47:52.127039Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-08-08T16:44:23.558695Z","lastLogin":"2023-04-07T07:52:12.808207Z","has2fa":true,"lastActive":"2023-04-18T17:53:21.169085Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"4548fe09d6c949ecb027c70af1d1e8fd"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-08-10T11:13:06.618597Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sdks-publish","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2023-02-01T19:03:04.268258Z","lastLogin":"2023-04-11T17:41:59.073515Z","has2fa":true,"lastActive":"2023-04-17T23:10:34.614992Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2023-02-03T19:52:50.750648Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-09-19T20:21:02.168949Z","lastLogin":"2023-04-17T20:27:01.536887Z","has2fa":true,"lastActive":"2023-04-18T21:09:26.940838Z","isSuperuser":false,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"9e67f1e04e184833b8b3bfa45beea7ea"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2023-01-05T15:31:45.227526Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sentry","snuba"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2021-08-10T13:59:05.267005Z","lastLogin":"2023-04-18T17:28:59.989484Z","has2fa":true,"lastActive":"2023-04-18T17:28:50.801466Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":"5b2c870c491841d0bc114c9df5bb6d2e"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2021-08-10T13:59:06.023687Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2021-08-05T22:12:41.902738Z","lastLogin":"2023-04-18T21:02:34.963627Z","has2fa":true,"lastActive":"2023-04-18T21:17:28.257879Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-09-16T18:05:51.667528Z","inviteStatus":"approved","inviterName":"richard.ma@sentry.io","orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-10-14T23:15:17.528252Z","lastLogin":"2023-03-31T20:53:37.291706Z","has2fa":false,"lastActive":"2023-04-18T20:12:14.438701Z","isSuperuser":false,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-10-19T19:38:46.936422Z","inviteStatus":"approved","inviterName":"isabel.matwawana@sentry.io","orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-12-09T23:20:23.346432Z","lastLogin":"2022-12-09T23:20:23.765878Z","has2fa":false,"lastActive":"2022-12-23T02:01:40.052033Z","isSuperuser":false,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":false,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-12-09T23:20:23.745693Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2018-02-26T23:57:43.766558Z","lastLogin":"2023-01-19T19:11:45.061167Z","has2fa":true,"lastActive":"2023-01-19T19:11:31.930426Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":false,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2018-02-26T16:04:56.738643Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-03-07T14:05:53.423324Z","lastLogin":"2023-04-18T07:29:18.311034Z","has2fa":true,"lastActive":"2023-04-18T16:17:56.241831Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"1836fd44387a413a917cf052523623cc"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-03-07T14:05:54.238336Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sdks-publish","sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2022-02-07T14:25:08.448480Z","lastLogin":"2023-04-13T08:24:55.740046Z","has2fa":true,"lastActive":"2023-04-18T17:53:19.914958Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"835ade1ffa314f788b1e1015af13cacb"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-02-07T14:25:08.946588Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sdks-publish","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2017-11-15T09:07:16.014572Z","lastLogin":"2023-04-18T17:50:00.282464Z","has2fa":true,"lastActive":"2023-04-18T17:55:17.126066Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2017-11-15T09:07:16.036013Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance","snuba"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2019-02-08T21:51:46.074436Z","lastLogin":"2023-04-18T20:03:13.403607Z","has2fa":true,"lastActive":"2023-04-18T20:35:34.150539Z","isSuperuser":true,"isStaff":true,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2019-02-08T21:51:46.795772Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":true,"isManaged":false,"dateJoined":"2022-06-27T18:14:49.652095Z","lastLogin":"2023-04-18T18:26:07.379897Z","has2fa":true,"lastActive":"2023-04-18T21:22:08.385362Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true},{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"upload","avatarUuid":"1fc882a55f7e43caad0069765a940d72"}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2022-06-27T18:14:50.014771Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["feedback","javascript","sentry","sentry-tests-acceptance"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.png","isActive":true,"hasPasswordAuth":false,"isManaged":false,"dateJoined":"2019-10-01T12:26:22.368368Z","lastLogin":"2023-04-18T16:28:34.978170Z","has2fa":true,"lastActive":"2023-04-18T17:53:44.185207Z","isSuperuser":true,"isStaff":false,"experiments":{},"emails":[{"id":"1234","email":"test-user-email@sentry.io","is_verified":true}],"avatar":{"avatarType":"letter_avatar","avatarUuid":null}},"role":"member","roleName":"Member","orgRole":"member","pending":false,"expired":false,"flags":{"idp:provisioned":false,"idp:role-restricted":false,"sso:linked":true,"sso:invalid":false,"member-limit:restricted":false},"dateCreated":"2019-10-01T12:26:22.971086Z","inviteStatus":"approved","inviterName":null,"orgRolesFromTeams":[],"projects":["javascript","sdks-publish","sentry"]},{"id":"1234","email":"test-user-email@sentry.io","name":"Sentry Test User","user":{"id":"1234","name":"Sentry Test User","username":"ABCD1234","email":"test-user-email@sentry.io","avatarUrl":"https://avatar-url.com/my-avatar.p diff --git a/packages/replay/test/fixtures/fixJson/2_completeJson.json b/packages/replay/test/fixtures/fixJson/2_completeJson.json new file mode 100644 index 000000000000..5c418e538d75 --- /dev/null +++ b/packages/replay/test/fixtures/fixJson/2_completeJson.json @@ -0,0 +1,22 @@ +[ + { + "id": "123456", + "email": "test.user@sentry.io", + "name": "test.user@sentry.io", + "user": { + "id": "123456", + "name": "test.user@sentry.io", + "username": "ABCDEF", + "email": "test.user@sentry.io", + "isActive": true + }, + "role": "member", + "roleName": "Member", + "orgRole": "member", + "pending": false, + "expired": false, + "dateCreated": "2021-09-24T13:30:02.620935Z", + "inviteStatus": "approved", + "inviterName": null, + "orgRolesFromTeams": [], + "projects": ["feedback", "javascript", "sentry", "sentry-tests-acceptance~~"]}] diff --git a/packages/replay/test/fixtures/fixJson/2_incompleteJson.txt b/packages/replay/test/fixtures/fixJson/2_incompleteJson.txt new file mode 100644 index 000000000000..cf7d63705010 --- /dev/null +++ b/packages/replay/test/fixtures/fixJson/2_incompleteJson.txt @@ -0,0 +1,22 @@ +[ + { + "id": "123456", + "email": "test.user@sentry.io", + "name": "test.user@sentry.io", + "user": { + "id": "123456", + "name": "test.user@sentry.io", + "username": "ABCDEF", + "email": "test.user@sentry.io", + "isActive": true + }, + "role": "member", + "roleName": "Member", + "orgRole": "member", + "pending": false, + "expired": false, + "dateCreated": "2021-09-24T13:30:02.620935Z", + "inviteStatus": "approved", + "inviterName": null, + "orgRolesFromTeams": [], + "projects": ["feedback", "javascript", "sentry", "sentry-tests-acceptance diff --git a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts index 809d3717f023..b47a849f868e 100644 --- a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts +++ b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts @@ -587,7 +587,7 @@ other-header: test`; ]); }); - it('skips fetch request/response body if configured & too large', async () => { + it('truncates fetch text request/response body if configured & too large', async () => { options.captureBodies = true; const breadcrumb: Breadcrumb = { @@ -634,15 +634,96 @@ other-header: test`; request: { size: LARGE_BODY.length, headers: {}, + body: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE)}…`, _meta: { - errors: ['MAX_BODY_SIZE_EXCEEDED'], + warnings: ['TEXT_TRUNCATED'], }, }, response: { size: LARGE_BODY.length, headers: {}, + body: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE)}…`, _meta: { - errors: ['MAX_BODY_SIZE_EXCEEDED'], + warnings: ['TEXT_TRUNCATED'], + }, + }, + }, + description: 'https://example.com', + endTimestamp: (BASE_TIMESTAMP + 2000) / 1000, + op: 'resource.fetch', + startTimestamp: (BASE_TIMESTAMP + 1000) / 1000, + }, + }, + }, + ]); + }); + + it('truncates fetch JSON request/response body if configured & too large', async () => { + options.captureBodies = true; + + const largeBody = JSON.stringify({ a: LARGE_BODY }); + + const breadcrumb: Breadcrumb = { + category: 'fetch', + data: { + method: 'GET', + url: 'https://example.com', + status_code: 200, + }, + }; + + const mockResponse = { + headers: { + get: () => '', + }, + clone: () => mockResponse, + text: () => Promise.resolve(largeBody), + } as unknown as Response; + + const hint: FetchBreadcrumbHint = { + input: ['GET', { body: largeBody }], + response: mockResponse, + startTimestamp: BASE_TIMESTAMP + 1000, + endTimestamp: BASE_TIMESTAMP + 2000, + }; + beforeAddNetworkBreadcrumb(options, breadcrumb, hint); + + expect(breadcrumb).toEqual({ + category: 'fetch', + data: { + method: 'GET', + request_body_size: largeBody.length, + status_code: 200, + url: 'https://example.com', + }, + }); + + await waitForReplayEventBuffer(); + + expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([ + { + type: 5, + timestamp: (BASE_TIMESTAMP + 1000) / 1000, + data: { + tag: 'performanceSpan', + payload: { + data: { + method: 'GET', + statusCode: 200, + request: { + size: largeBody.length, + headers: {}, + body: { a: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE - 6)}~~` }, + _meta: { + warnings: ['JSON_TRUNCATED'], + }, + }, + response: { + size: largeBody.length, + headers: {}, + body: { a: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE - 6)}~~` }, + _meta: { + warnings: ['JSON_TRUNCATED'], }, }, }, @@ -854,7 +935,7 @@ other-header: test`; ]); }); - it('skip xhr request/response body if configured & body too large', async () => { + it('truncates text xhr request/response body if configured & body too large', async () => { options.captureBodies = true; const breadcrumb: Breadcrumb = { @@ -906,15 +987,95 @@ other-header: test`; request: { size: LARGE_BODY.length, headers: {}, + body: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE)}…`, _meta: { - errors: ['MAX_BODY_SIZE_EXCEEDED'], + warnings: ['TEXT_TRUNCATED'], }, }, response: { size: LARGE_BODY.length, headers: {}, + body: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE)}…`, + _meta: { + warnings: ['TEXT_TRUNCATED'], + }, + }, + }, + description: 'https://example.com', + endTimestamp: (BASE_TIMESTAMP + 2000) / 1000, + op: 'resource.xhr', + startTimestamp: (BASE_TIMESTAMP + 1000) / 1000, + }, + }, + }, + ]); + }); + + it('truncates JSON xhr request/response body if configured & body too large', async () => { + options.captureBodies = true; + + const largeBody = JSON.stringify({ a: LARGE_BODY }); + + const breadcrumb: Breadcrumb = { + category: 'xhr', + data: { + method: 'GET', + url: 'https://example.com', + status_code: 200, + }, + }; + const xhr = new XMLHttpRequest(); + Object.defineProperty(xhr, 'response', { + value: largeBody, + }); + Object.defineProperty(xhr, 'responseText', { + value: largeBody, + }); + const hint: XhrBreadcrumbHint = { + xhr, + input: largeBody, + startTimestamp: BASE_TIMESTAMP + 1000, + endTimestamp: BASE_TIMESTAMP + 2000, + }; + beforeAddNetworkBreadcrumb(options, breadcrumb, hint); + + expect(breadcrumb).toEqual({ + category: 'xhr', + data: { + method: 'GET', + request_body_size: largeBody.length, + response_body_size: largeBody.length, + status_code: 200, + url: 'https://example.com', + }, + }); + + await waitForReplayEventBuffer(); + + expect((options.replay.eventBuffer as EventBufferArray).events).toEqual([ + { + type: 5, + timestamp: (BASE_TIMESTAMP + 1000) / 1000, + data: { + tag: 'performanceSpan', + payload: { + data: { + method: 'GET', + statusCode: 200, + request: { + size: largeBody.length, + headers: {}, + body: { a: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE - 6)}~~` }, + _meta: { + warnings: ['JSON_TRUNCATED'], + }, + }, + response: { + size: largeBody.length, + headers: {}, + body: { a: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE - 6)}~~` }, _meta: { - errors: ['MAX_BODY_SIZE_EXCEEDED'], + warnings: ['JSON_TRUNCATED'], }, }, }, diff --git a/packages/replay/test/unit/coreHandlers/util/networkUtils.test.ts b/packages/replay/test/unit/coreHandlers/util/networkUtils.test.ts index 2d407221b77a..f187fbe59de0 100644 --- a/packages/replay/test/unit/coreHandlers/util/networkUtils.test.ts +++ b/packages/replay/test/unit/coreHandlers/util/networkUtils.test.ts @@ -1,6 +1,10 @@ import { TextEncoder } from 'util'; -import { getBodySize, parseContentLengthHeader } from '../../../../src/coreHandlers/util/networkUtils'; +import { + buildNetworkRequestOrResponse, + getBodySize, + parseContentLengthHeader, +} from '../../../../src/coreHandlers/util/networkUtils'; jest.useFakeTimers(); @@ -62,4 +66,125 @@ describe('Unit | coreHandlers | util | networkUtils', () => { expect(getBodySize(arrayBuffer, textEncoder)).toBe(8); }); }); + + describe('buildNetworkRequestOrResponse', () => { + it.each([ + ['just text', 'just text', undefined], + ['[invalid JSON]', '[invalid JSON]', { warnings: ['INVALID_JSON'] }], + ['{invalid JSON}', '{invalid JSON}', { warnings: ['INVALID_JSON'] }], + ['[]', [], undefined], + [JSON.stringify([1, 'a', true, null, undefined]), [1, 'a', true, null, null], undefined], + [JSON.stringify([1, [2, [3, [4, [5, [6, [7, [8]]]]]]]]), [1, [2, [3, [4, [5, [6, [7, [8]]]]]]]], undefined], + ['{}', {}, undefined], + [ + JSON.stringify({ a: 1, b: true, c: 'yes', d: null, e: undefined }), + { a: 1, b: true, c: 'yes', d: null, e: undefined }, + undefined, + ], + [ + JSON.stringify({ + a: 1, + b: { + c: 2, + d: { + e: 3, + f: { + g: 4, + h: { + i: 5, + j: { + k: 6, + l: { + m: 7, + n: { + o: 8, + }, + }, + }, + }, + }, + }, + }, + }), + { + a: 1, + b: { + c: 2, + d: { + e: 3, + f: { + g: 4, + h: { + i: 5, + j: { + k: 6, + l: { + m: 7, + n: { + o: 8, + }, + }, + }, + }, + }, + }, + }, + }, + undefined, + ], + [ + JSON.stringify({ + data: { + user: { + name: 'John', + age: 42, + friends: [ + { + name: 'Jane', + }, + { + name: 'Bob', + children: [ + { name: 'Alice' }, + { + name: 'Rose', + hobbies: [{ name: 'Dancing' }, { name: 'Programming' }, { name: 'Dueling' }], + }, + ], + }, + ], + }, + }, + }), + { + data: { + user: { + name: 'John', + age: 42, + friends: [ + { + name: 'Jane', + }, + { + name: 'Bob', + children: [ + { name: 'Alice' }, + { + name: 'Rose', + hobbies: [{ name: 'Dancing' }, { name: 'Programming' }, { name: 'Dueling' }], + }, + ], + }, + ], + }, + }, + }, + undefined, + ], + ])('works with %s', (input, expectedBody, expectedMeta) => { + const actual = buildNetworkRequestOrResponse({}, 1, input); + + expect(actual).toEqual({ size: 1, headers: {}, body: expectedBody, _meta: expectedMeta }); + }); + }); }); diff --git a/packages/replay/test/unit/coreHandlers/util/truncateJson/fixJson.test.ts b/packages/replay/test/unit/coreHandlers/util/truncateJson/fixJson.test.ts new file mode 100644 index 000000000000..d7c294b42262 --- /dev/null +++ b/packages/replay/test/unit/coreHandlers/util/truncateJson/fixJson.test.ts @@ -0,0 +1,82 @@ +import fs from 'fs'; +import path from 'path'; + +import { fixJson } from '../../../../../src/util/truncateJson/fixJson'; + +describe('Unit | coreHandlers | util | truncateJson | fixJson', () => { + test.each([ + // Basic steps of object completion + ['{', '{"~~":"~~"}'], + ['{}', '{}'], + ['{"', '{"~~":"~~"}'], + ['{"a', '{"a~~":"~~"}'], + ['{"aa', '{"aa~~":"~~"}'], + ['{"aa"', '{"aa":"~~"}'], + ['{"aa":', '{"aa":"~~"}'], + ['{"aa":"', '{"aa":"~~"}'], + ['{"aa":"b', '{"aa":"b~~"}'], + ['{"aa":"bb', '{"aa":"bb~~"}'], + ['{"aa":"bb"', '{"aa":"bb","~~":"~~"}'], + ['{"aa":"bb"}', '{"aa":"bb"}'], + + // Basic steps of array completion + ['[', '["~~"]'], + ['[]', '[]'], + ['["', '["~~"]'], + ['["a', '["a~~"]'], + ['["aa', '["aa~~"]'], + ['["aa"', '["aa","~~"]'], + ['["aa",', '["aa","~~"]'], + ['["aa","', '["aa","~~"]'], + ['["aa","b', '["aa","b~~"]'], + ['["aa","bb', '["aa","bb~~"]'], + ['["aa","bb"', '["aa","bb","~~"]'], + ['["aa","bb"]', '["aa","bb"]'], + + // Nested object/arrays + ['{"a":{"bb', '{"a":{"bb~~":"~~"}}'], + ['{"a":["bb",["cc","d', '{"a":["bb",["cc","d~~"]]}'], + + // Handles special characters in strings + ['{"a":"hel\\"lo', '{"a":"hel\\"lo~~"}'], + ['{"a":["this is }{some][ thing', '{"a":["this is }{some][ thing~~"]}'], + ['{"a:a', '{"a:a~~":"~~"}'], + ['{"a:', '{"a:~~":"~~"}'], + + // Handles incomplete non-string values + ['{"a":true', '{"a":true,"~~":"~~"}'], + ['{"a":false', '{"a":false,"~~":"~~"}'], + ['{"a":null', '{"a":null,"~~":"~~"}'], + ['{"a":tr', '{"a":"~~"}'], + ['{"a":1', '{"a":"~~"}'], + ['{"a":12', '{"a":"~~"}'], + ['[12', '["~~"]'], + ['[true', '[true,"~~"]'], + ['{"a":1', '{"a":"~~"}'], + ['{"a":tr', '{"a":"~~"}'], + ['{"a":true', '{"a":true,"~~":"~~"}'], + + // Handles whitespace + ['{"a" : true', '{"a" : true,"~~":"~~"}'], + ['{"a" : "aa', '{"a" : "aa~~"}'], + ['[1, 2, "a ", ', '[1, 2, "a ","~~"]'], + ['[1, 2, true ', '[1, 2, true ,"~~"]'], + // Complex nested JSON + ['{"aa":{"bb":"yes","cc":true},"xx":["aa",1,true', '{"aa":{"bb":"yes","cc":true},"xx":["aa",1,true,"~~"]}'], + ])('it works for %s', (json, expected) => { + const actual = fixJson(json); + expect(actual).toEqual(expected); + }); + + test.each(['1', '2'])('it works for fixture %s_incompleteJson.txt', fixture => { + const input = fs + .readFileSync(path.resolve(__dirname, `../../../../fixtures/fixJson/${fixture}_incompleteJson.txt`), 'utf8') + .trim(); + const expected = fs + .readFileSync(path.resolve(__dirname, `../../../../fixtures/fixJson/${fixture}_completeJson.json`), 'utf8') + .trim(); + + const actual = fixJson(input); + expect(actual).toEqual(expected); + }); +});