diff --git a/.babelrc b/.babelrc index 7fe9a63a1..b61790df4 100644 --- a/.babelrc +++ b/.babelrc @@ -1,13 +1,26 @@ { - "presets": ["@babel/env", ["@babel/typescript", { "jsxPragma": "h" }]], - "plugins": [ - "@babel/plugin-transform-nullish-coalescing-operator", - [ - "@babel/transform-react-jsx", - { - "runtime": "automatic", - "importSource": "preact" - } - ] + "presets": [ + [ + "@babel/env", + { + "targets": ">0.5%, last 2 versions, Firefox ESR, not dead" + } + ], + [ + "@babel/typescript", + { + "jsxPragma": "h" + } ] + ], + "plugins": [ + "@babel/plugin-transform-nullish-coalescing-operator", + [ + "@babel/transform-react-jsx", + { + "runtime": "automatic", + "importSource": "preact" + } + ] + ] } diff --git a/package.json b/package.json index 820898e72..701a65ab0 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ ] }, "browserslist": [ - "> 0.5%, last 2 versions, Firefox ESR, not dead, IE 11" + ">0.5%, last 2 versions, Firefox ESR, not dead" ], "pnpm": { "patchedDependencies": { diff --git a/playground/segment/server.js b/playground/segment/server.js index 8c67259a1..c99ef4712 100644 --- a/playground/segment/server.js +++ b/playground/segment/server.js @@ -1,19 +1,25 @@ +// eslint-disable-next-line @typescript-eslint/no-require-imports,no-undef const path = require('path') +// eslint-disable-next-line @typescript-eslint/no-require-imports,no-undef const express = require('express') const app = express() const port = 3001 +// eslint-disable-next-line no-undef app.use('/static', express.static(__dirname + '/../../dist')) app.get('/segment.html', function (req, res) { + // eslint-disable-next-line no-undef res.sendFile(__dirname + '/segment.html') }) app.get('/static/recorder.js', function (req, res) { + // eslint-disable-next-line no-undef let filePath = path.join(__dirname, '/../../node_modules/rrweb/dist/rrweb.js') res.sendFile(filePath) }) app.listen(port, () => { + // eslint-disable-next-line no-console console.log(`Example Segment app listening on port ${port}`) }) diff --git a/src/__tests__/extensions/replay/sessionrecording-utils.test.ts b/src/__tests__/extensions/replay/sessionrecording-utils.test.ts index d0faef5b1..521263f42 100644 --- a/src/__tests__/extensions/replay/sessionrecording-utils.test.ts +++ b/src/__tests__/extensions/replay/sessionrecording-utils.test.ts @@ -193,7 +193,8 @@ describe(`SessionRecording utility functions`, () => { data: { plugin: CONSOLE_LOG_PLUGIN_NAME, payload: { - payload: ['a', largeString.slice(0, 2000) + '...[truncated]'], + // 14 is the length of the '...[truncated]' string + payload: ['a', largeString.slice(0, 2000 - 14) + '...[truncated]'], }, }, }) diff --git a/src/__tests__/utils/string-utils.test.ts b/src/__tests__/utils/string-utils.test.ts new file mode 100644 index 000000000..4cc874b05 --- /dev/null +++ b/src/__tests__/utils/string-utils.test.ts @@ -0,0 +1,37 @@ +import { truncateString } from '../../utils/string-utils' + +describe('string-utils', () => { + it.each([ + // Basic string truncation without suffix + ['hello world', 5, undefined, 'hello'], + + // Basic string truncation without suffix + ['hello world ', 15, undefined, 'hello world'], + + // String with surrogate pair (emoji) + ['hello 😄 world', 7, undefined, 'hello 😄'], + + // String with surrogate pair, truncated in the middle of the emoji + ['hello 😄 world', 6, undefined, 'hello'], + + // Truncation with a suffix added + ['hello world', 5, '...', 'he...'], + + // Handling whitespace and suffix + [' hello world ', 7, '...', 'hell...'], + + // Empty string with suffix + ['', 5, '-', ''], + + // invalid input string with suffix + [null, 5, '-', ''], + + // Truncation without a suffix and with an emoji + ['hello 😄 world', 8, undefined, 'hello 😄'], + ])( + 'should truncate string "%s" to max length %d with suffix "%s" and return "%s"', + (input: string, maxLength: number, suffix: string | undefined, expected: string) => { + expect(truncateString(input, maxLength, suffix)).toEqual(expected) + } + ) +}) diff --git a/src/extensions/replay/sessionrecording-utils.ts b/src/extensions/replay/sessionrecording-utils.ts index 02270bdf3..c2376d8dd 100644 --- a/src/extensions/replay/sessionrecording-utils.ts +++ b/src/extensions/replay/sessionrecording-utils.ts @@ -1,8 +1,9 @@ import type { eventWithTime, listenerHandler, pluginEvent } from '@rrweb/types' import type { record } from '@rrweb/record' -import { isObject } from '../../utils/type-utils' +import { isNullish, isObject } from '../../utils/type-utils' import { SnapshotBuffer } from './sessionrecording' +import { truncateString } from '../../utils/string-utils' // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references export function circularReferenceReplacer() { @@ -109,15 +110,15 @@ export function truncateLargeConsoleLogs(_event: eventWithTime) { } const updatedPayload = [] for (let i = 0; i < event.data.payload.payload.length; i++) { - if ( - event.data.payload.payload[i] && // Value can be null - event.data.payload.payload[i].length > MAX_STRING_SIZE - ) { - updatedPayload.push(event.data.payload.payload[i].slice(0, MAX_STRING_SIZE) + '...[truncated]') - } else { + if (isNullish(event.data.payload.payload[i])) { + // we expect nullish values to be logged unchanged, + // TODO test if this is still necessary https://github.com/PostHog/posthog-js/pull/385 updatedPayload.push(event.data.payload.payload[i]) + } else { + updatedPayload.push(truncateString(event.data.payload.payload[i], MAX_STRING_SIZE, '...[truncated]')) } } + event.data.payload.payload = updatedPayload // Return original type return _event diff --git a/src/utils/index.ts b/src/utils/index.ts index 39d4bec8e..a9df90803 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,6 +2,7 @@ import { Breaker, EventHandler, Properties } from '../types' import { hasOwnProperty, isArray, isFormData, isFunction, isNull, isNullish, isString } from './type-utils' import { logger } from './logger' import { nativeForEach, nativeIndexOf, window } from './globals' +import { truncateString } from './string-utils' const breaker: Breaker = {} @@ -207,7 +208,7 @@ export function _copyAndTruncateStrings = Record { if (isString(value) && !isNull(maxStringLength)) { - return (value as string).slice(0, maxStringLength) + return truncateString(value, maxStringLength) } return value }) as T diff --git a/src/utils/string-utils.ts b/src/utils/string-utils.ts new file mode 100644 index 000000000..06c965b27 --- /dev/null +++ b/src/utils/string-utils.ts @@ -0,0 +1,19 @@ +/** + * Truncate a string to a maximum length and optionally append a suffix only if the string is longer than the maximum length. + */ +export const truncateString = (str: unknown, maxLength: number, suffix?: string): string => { + if (typeof str !== 'string') { + return '' + } + + const trimmedSuffix = suffix?.trim() + const trimmedStr = str.trim() + + let sliceLength = maxLength + if (trimmedSuffix?.length) { + sliceLength -= trimmedSuffix.length + } + const sliced = Array.from(trimmedStr).slice(0, sliceLength).join('').trim() + const addSuffix = trimmedStr.length > maxLength + return sliced + (addSuffix ? trimmedSuffix || '' : '') +}