diff --git a/packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts b/packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts index 13353bf1fb6c..d7052b1384ad 100644 --- a/packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts +++ b/packages/browser-integration-tests/suites/replay/captureConsoleLog/test.ts @@ -99,23 +99,7 @@ sentryTest('should capture very large console logs', async ({ getLocalTestPath, type: 'default', category: 'console', data: { - arguments: [ - expect.objectContaining({ - 'item-0': { - aa: expect.objectContaining({ - 'item-0': { - aa: expect.any(Object), - bb: expect.any(String), - cc: expect.any(String), - dd: expect.any(String), - }, - }), - bb: expect.any(String), - cc: expect.any(String), - dd: expect.any(String), - }, - }), - ], + arguments: [expect.any(String)], logger: 'console', _meta: { warnings: ['CONSOLE_ARG_TRUNCATED'], diff --git a/packages/replay/src/coreHandlers/handleScope.ts b/packages/replay/src/coreHandlers/handleScope.ts index 78d0b6dd3fd3..5366fdf16aeb 100644 --- a/packages/replay/src/coreHandlers/handleScope.ts +++ b/packages/replay/src/coreHandlers/handleScope.ts @@ -5,7 +5,6 @@ import { CONSOLE_ARG_MAX_SIZE } from '../constants'; import type { ReplayContainer } from '../types'; import type { ReplayFrame } from '../types/replayFrame'; import { createBreadcrumb } from '../util/createBreadcrumb'; -import { fixJson } from '../util/truncateJson/fixJson'; import { addBreadcrumbEvent } from './util/addBreadcrumbEvent'; let _LAST_BREADCRUMB: null | Breadcrumb = null; @@ -95,11 +94,9 @@ export function normalizeConsoleBreadcrumb( const normalizedArg = normalize(arg, 7); const stringified = JSON.stringify(normalizedArg); if (stringified.length > CONSOLE_ARG_MAX_SIZE) { - const fixedJson = fixJson(stringified.slice(0, CONSOLE_ARG_MAX_SIZE)); - const json = JSON.parse(fixedJson); - // We only set this after JSON.parse() was successfull, so we know we didn't run into `catch` isTruncated = true; - return json; + // We use the pretty printed JSON string here as a base + return `${JSON.stringify(normalizedArg, null, 2).slice(0, CONSOLE_ARG_MAX_SIZE)}…`; } return normalizedArg; } catch { diff --git a/packages/replay/src/coreHandlers/util/networkUtils.ts b/packages/replay/src/coreHandlers/util/networkUtils.ts index 130db1658354..78b4e22efa9b 100644 --- a/packages/replay/src/coreHandlers/util/networkUtils.ts +++ b/packages/replay/src/coreHandlers/util/networkUtils.ts @@ -10,7 +10,6 @@ import type { ReplayNetworkRequestOrResponse, ReplayPerformanceEntry, } from '../../types'; -import { fixJson } from '../../util/truncateJson/fixJson'; /** Get the size of a body. */ export function getBodySize( @@ -161,7 +160,7 @@ export function buildNetworkRequestOrResponse( const { body: normalizedBody, warnings } = normalizeNetworkBody(body); info.body = normalizedBody; - if (warnings.length > 0) { + if (warnings && warnings.length > 0) { info._meta = { warnings, }; @@ -191,36 +190,47 @@ function _serializeFormData(formData: FormData): string { function normalizeNetworkBody(body: string | undefined): { body: NetworkBody | undefined; - warnings: NetworkMetaWarning[]; + 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); + const isProbablyJson = _strIsProbablyJson(body); + + if (exceedsSizeLimit) { + const truncatedBody = body.slice(0, NETWORK_BODY_MAX_SIZE); + + if (isProbablyJson) { return { - body: normalizedBody, - warnings: exceedsSizeLimit ? ['JSON_TRUNCATED'] : [], + body: truncatedBody, + warnings: ['MAYBE_JSON_TRUNCATED'], }; - } catch { + } + + return { + body: `${truncatedBody}…`, + warnings: ['TEXT_TRUNCATED'], + }; + } + + if (isProbablyJson) { + try { + const jsonBody = JSON.parse(body); return { - body: exceedsSizeLimit ? `${body.slice(0, NETWORK_BODY_MAX_SIZE)}…` : body, - warnings: exceedsSizeLimit ? ['INVALID_JSON', 'TEXT_TRUNCATED'] : ['INVALID_JSON'], + body: jsonBody, }; + } catch { + // fall back to just send the body as string } } return { - body: exceedsSizeLimit ? `${body.slice(0, NETWORK_BODY_MAX_SIZE)}…` : body, - warnings: exceedsSizeLimit ? ['TEXT_TRUNCATED'] : [], + body, }; } diff --git a/packages/replay/src/types/request.ts b/packages/replay/src/types/request.ts index ae24574d4b7e..1e5692f901e9 100644 --- a/packages/replay/src/types/request.ts +++ b/packages/replay/src/types/request.ts @@ -3,7 +3,7 @@ type JsonArray = unknown[]; export type NetworkBody = JsonObject | JsonArray | string; -export type NetworkMetaWarning = 'JSON_TRUNCATED' | 'TEXT_TRUNCATED' | 'INVALID_JSON' | 'URL_SKIPPED'; +export type NetworkMetaWarning = 'MAYBE_JSON_TRUNCATED' | 'TEXT_TRUNCATED' | 'URL_SKIPPED'; interface NetworkMeta { warnings?: NetworkMetaWarning[]; diff --git a/packages/replay/src/util/truncateJson/completeJson.ts b/packages/replay/src/util/truncateJson/completeJson.ts deleted file mode 100644 index 3e7be2f38a13..000000000000 --- a/packages/replay/src/util/truncateJson/completeJson.ts +++ /dev/null @@ -1,125 +0,0 @@ -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 deleted file mode 100644 index 6ea4f2dda3e2..000000000000 --- a/packages/replay/src/util/truncateJson/constants.ts +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 0ba8d79c4c9a..000000000000 --- a/packages/replay/src/util/truncateJson/evaluateJson.ts +++ /dev/null @@ -1,264 +0,0 @@ -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 deleted file mode 100644 index b54d80f011c3..000000000000 --- a/packages/replay/src/util/truncateJson/fixJson.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* 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/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts index c0ad9ef59b49..24b7aa6414e9 100644 --- a/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts +++ b/packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts @@ -785,17 +785,17 @@ other-header: test`; request: { size: largeBody.length, headers: {}, - body: { a: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE - 6)}~~` }, + body: largeBody.slice(0, NETWORK_BODY_MAX_SIZE), _meta: { - warnings: ['JSON_TRUNCATED'], + warnings: ['MAYBE_JSON_TRUNCATED'], }, }, response: { size: largeBody.length, headers: {}, - body: { a: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE - 6)}~~` }, + body: largeBody.slice(0, NETWORK_BODY_MAX_SIZE), _meta: { - warnings: ['JSON_TRUNCATED'], + warnings: ['MAYBE_JSON_TRUNCATED'], }, }, }, @@ -1211,17 +1211,17 @@ other-header: test`; request: { size: largeBody.length, headers: {}, - body: { a: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE - 6)}~~` }, + body: largeBody.slice(0, NETWORK_BODY_MAX_SIZE), _meta: { - warnings: ['JSON_TRUNCATED'], + warnings: ['MAYBE_JSON_TRUNCATED'], }, }, response: { size: largeBody.length, headers: {}, - body: { a: `${LARGE_BODY.slice(0, NETWORK_BODY_MAX_SIZE - 6)}~~` }, + body: largeBody.slice(0, NETWORK_BODY_MAX_SIZE), _meta: { - warnings: ['JSON_TRUNCATED'], + warnings: ['MAYBE_JSON_TRUNCATED'], }, }, }, diff --git a/packages/replay/test/unit/coreHandlers/handleScope.test.ts b/packages/replay/test/unit/coreHandlers/handleScope.test.ts index 894f49592f93..ebf6373ed54b 100644 --- a/packages/replay/test/unit/coreHandlers/handleScope.test.ts +++ b/packages/replay/test/unit/coreHandlers/handleScope.test.ts @@ -114,15 +114,14 @@ describe('Unit | coreHandlers | handleScope', () => { }); it('truncates large JSON objects', () => { + const bb = { bb: 'b'.repeat(CONSOLE_ARG_MAX_SIZE + 10) }; + const c = { c: 'c'.repeat(CONSOLE_ARG_MAX_SIZE + 10) }; + const breadcrumb = { category: 'console', message: 'test', data: { - arguments: [ - { aa: 'yes' }, - { bb: 'b'.repeat(CONSOLE_ARG_MAX_SIZE + 10) }, - { c: 'c'.repeat(CONSOLE_ARG_MAX_SIZE + 10) }, - ], + arguments: [{ aa: 'yes' }, bb, c], }, }; const actual = HandleScope.normalizeConsoleBreadcrumb(breadcrumb); @@ -133,8 +132,8 @@ describe('Unit | coreHandlers | handleScope', () => { data: { arguments: [ { aa: 'yes' }, - { bb: `${'b'.repeat(CONSOLE_ARG_MAX_SIZE - 7)}~~` }, - { c: `${'c'.repeat(CONSOLE_ARG_MAX_SIZE - 6)}~~` }, + `${JSON.stringify(bb, null, 2).slice(0, CONSOLE_ARG_MAX_SIZE)}…`, + `${JSON.stringify(c, null, 2).slice(0, CONSOLE_ARG_MAX_SIZE)}…`, ], _meta: { warnings: ['CONSOLE_ARG_TRUNCATED'] }, }, diff --git a/packages/replay/test/unit/coreHandlers/util/networkUtils.test.ts b/packages/replay/test/unit/coreHandlers/util/networkUtils.test.ts index 04f67087c272..f00acad6384d 100644 --- a/packages/replay/test/unit/coreHandlers/util/networkUtils.test.ts +++ b/packages/replay/test/unit/coreHandlers/util/networkUtils.test.ts @@ -72,8 +72,8 @@ describe('Unit | coreHandlers | util | networkUtils', () => { describe('buildNetworkRequestOrResponse', () => { it.each([ ['just text', 'just text', undefined], - ['[invalid JSON]', '[invalid JSON]', { warnings: ['INVALID_JSON'] }], - ['{invalid JSON}', '{invalid JSON}', { warnings: ['INVALID_JSON'] }], + ['[invalid JSON]', '[invalid JSON]', undefined], + ['{invalid JSON}', '{invalid JSON}', undefined], ['[]', [], 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], @@ -195,10 +195,10 @@ describe('Unit | coreHandlers | util | networkUtils', () => { JSON.stringify({ aa: 'a'.repeat(NETWORK_BODY_MAX_SIZE + 10), }), - { - aa: `${'a'.repeat(NETWORK_BODY_MAX_SIZE - 7)}~~`, - }, - { warnings: ['JSON_TRUNCATED'] }, + JSON.stringify({ + aa: 'a'.repeat(NETWORK_BODY_MAX_SIZE + 10), + }).slice(0, NETWORK_BODY_MAX_SIZE), + { warnings: ['MAYBE_JSON_TRUNCATED'] }, ], [ 'large plain string', @@ -211,8 +211,11 @@ describe('Unit | coreHandlers | util | networkUtils', () => { `{--${JSON.stringify({ aa: 'a'.repeat(NETWORK_BODY_MAX_SIZE + 10), })}`, - `{--{"aa":"${'a'.repeat(NETWORK_BODY_MAX_SIZE - 10)}…`, - { warnings: ['INVALID_JSON', 'TEXT_TRUNCATED'] }, + + `{--${JSON.stringify({ + aa: 'a'.repeat(NETWORK_BODY_MAX_SIZE + 10), + })}`.slice(0, NETWORK_BODY_MAX_SIZE), + { warnings: ['MAYBE_JSON_TRUNCATED'] }, ], ])('works with %s', (label, input, expectedBody, expectedMeta) => { const actual = buildNetworkRequestOrResponse({}, 1, input); diff --git a/packages/replay/test/unit/coreHandlers/util/truncateJson/fixJson.test.ts b/packages/replay/test/unit/coreHandlers/util/truncateJson/fixJson.test.ts deleted file mode 100644 index d7c294b42262..000000000000 --- a/packages/replay/test/unit/coreHandlers/util/truncateJson/fixJson.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -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); - }); -});