diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index ed8366cb92..6337b0c46e 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -1,11 +1,7 @@ Component,Origin,License,Copyright -require,lodash.assign,MIT,Copyright jQuery Foundation and other contributors -require,lodash.merge,MIT,Copyright OpenJS Foundation and other contributors require,tslib,Apache-2.0,Copyright Microsoft Corporation file,tracekit,MIT,Copyright 2013 Onur Can Cakmak and all TraceKit contributors dev,@types/jasmine,MIT,Copyright Microsoft Corporation -dev,@types/lodash.assign,MIT,Copyright Microsoft Corporation -dev,@types/lodash.merge,MIT,Copyright Microsoft Corporation dev,@types/request,MIT,Copyright Microsoft Corporation dev,@types/sinon,MIT,Copyright Microsoft Corporation dev,@wdio/browserstack-service,MIT,Copyright JS Foundation and other contributors diff --git a/packages/core/package.json b/packages/core/package.json index f99670c0fa..0313c72def 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -11,13 +11,9 @@ "build:esm": "rm -rf esm && tsc -p tsconfig.esm.json" }, "dependencies": { - "lodash.assign": "4.2.0", - "lodash.merge": "4.6.2", "tslib": "1.10.0" }, "devDependencies": { - "@types/lodash.assign": "4.2.6", - "@types/lodash.merge": "4.6.6", "@types/sinon": "7.0.13", "sinon": "7.3.2" }, diff --git a/packages/core/src/internalMonitoring.ts b/packages/core/src/internalMonitoring.ts index f244c63ee2..61b05f3b00 100644 --- a/packages/core/src/internalMonitoring.ts +++ b/packages/core/src/internalMonitoring.ts @@ -1,8 +1,4 @@ // tslint:disable ban-types - -import lodashAssign from 'lodash.assign' -import lodashMerge from 'lodash.merge' - import { Configuration } from './configuration' import { toStackTraceString } from './errorCollection' import { computeStackTrace } from './tracekit' @@ -45,7 +41,7 @@ export function startInternalMonitoring(configuration: Configuration): InternalM configuration.maxMessageSize, configuration.flushTimeout, () => - lodashMerge( + utils.deepMerge( { date: new Date().getTime(), view: { @@ -54,10 +50,10 @@ export function startInternalMonitoring(configuration: Configuration): InternalM }, }, externalContextProvider !== undefined ? externalContextProvider() : {} - ) + ) as utils.Context ) - lodashAssign(monitoringConfiguration, { + utils.assign(monitoringConfiguration, { batch, maxMessagesPerPage: configuration.maxInternalMonitoringMessagesPerPage, sentMessageCount: 0, diff --git a/packages/core/src/transport.ts b/packages/core/src/transport.ts index 5f68219370..ae381f5bfe 100644 --- a/packages/core/src/transport.ts +++ b/packages/core/src/transport.ts @@ -1,7 +1,5 @@ -import lodashMerge from 'lodash.merge' - import { monitor } from './internalMonitoring' -import { Context, DOM_EVENT, jsonStringify, noop, objectValues } from './utils' +import { Context, deepMerge, DOM_EVENT, jsonStringify, noop, objectValues } from './utils' /** * Use POST request without content type to: @@ -89,7 +87,7 @@ export class Batch { } private process(message: T) { - const contextualizedMessage = lodashMerge({}, this.contextProvider(), message) as Context + const contextualizedMessage = deepMerge({}, this.contextProvider(), (message as unknown) as Context) as Context const processedMessage = jsonStringify(contextualizedMessage)! const messageBytesSize = this.sizeInBytes(processedMessage) return { processedMessage, messageBytesSize } diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index ad73808470..a3130f3c63 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -72,6 +72,52 @@ export function throttle( } } +const isContextArray = (value: ContextValue): value is ContextArray => Array.isArray(value) +const isContext = (value: ContextValue): value is Context => !Array.isArray(value) && typeof value === 'object' + +/** + * Performs a deep merge of objects and arrays + * - arrays values are merged index by index + * - objects are merged by keys + * - values get replaced, unless undefined + * + * ⚠️ this method does not prevent infinite loops while merging circular references ⚠️ + * + */ +export function deepMerge(destination: ContextValue, ...toMerge: ContextValue[]): ContextValue { + return toMerge.reduce((value1: ContextValue, value2: ContextValue): ContextValue => { + if (isContextArray(value1) && isContextArray(value2)) { + return [...Array(Math.max(value1.length, value2.length))].map((_, index) => + deepMerge(value1[index], value2[index]) + ) + } + if (isContext(value1) && isContext(value2)) { + return Object.keys(value2).reduce( + (merged, key) => ({ + ...merged, + [key]: deepMerge(value1[key], value2[key]), + }), + value1 + ) + } + return value2 === undefined ? value1 : value2 + }, destination) +} + +interface Assignable { + [key: string]: any +} + +export function assign(target: Assignable, ...toAssign: Assignable[]) { + toAssign.forEach((source: Assignable) => { + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key] + } + } + }) +} + /** * UUID v4 * from https://gist.github.com/jed/982883 diff --git a/packages/core/test/utils.spec.ts b/packages/core/test/utils.spec.ts index aebcd51597..c29ba3b309 100644 --- a/packages/core/test/utils.spec.ts +++ b/packages/core/test/utils.spec.ts @@ -1,6 +1,24 @@ -import { jsonStringify, performDraw, round, throttle, toSnakeCase, withSnakeCaseKeys } from '../src/utils' +import { deepMerge, jsonStringify, performDraw, round, throttle, toSnakeCase, withSnakeCaseKeys } from '../src/utils' describe('utils', () => { + describe('deepMerge', () => { + it('should deeply add and replace keys', () => { + const target = { a: { b: 'toBeReplaced', c: 'target' } } + const source = { a: { b: 'replaced', d: 'source' } } + expect(deepMerge(target, source)).toEqual({ a: { b: 'replaced', c: 'target', d: 'source' } }) + }) + + it('should not replace with undefined', () => { + expect(deepMerge({ a: 1 }, { a: undefined })).toEqual({ a: 1 }) + }) + + it('should merge arrays', () => { + const target = [{ a: 'target' }, 'extraString'] + const source = [{ b: 'source' }] + expect(deepMerge(target, source)).toEqual([{ a: 'target', b: 'source' }, 'extraString']) + }) + }) + describe('throttle', () => { let spy: jasmine.Spy let throttled: () => void diff --git a/packages/logs/package.json b/packages/logs/package.json index 9885169585..bc5d617990 100644 --- a/packages/logs/package.json +++ b/packages/logs/package.json @@ -14,13 +14,9 @@ }, "dependencies": { "@datadog/browser-core": "1.11.6", - "lodash.assign": "4.2.0", - "lodash.merge": "4.6.2", "tslib": "1.10.0" }, "devDependencies": { - "@types/lodash.assign": "4.2.6", - "@types/lodash.merge": "4.6.6", "@types/sinon": "7.0.13", "sinon": "7.3.2" }, diff --git a/packages/logs/src/logger.ts b/packages/logs/src/logger.ts index 2a8108a20a..9b80b796c4 100644 --- a/packages/logs/src/logger.ts +++ b/packages/logs/src/logger.ts @@ -3,6 +3,7 @@ import { Configuration, Context, ContextValue, + deepMerge, ErrorMessage, ErrorObservable, ErrorOrigin, @@ -12,7 +13,6 @@ import { monitored, noop, } from '@datadog/browser-core' -import lodashMerge from 'lodash.merge' import { LoggerSession } from './loggerSession' import { LogsGlobal } from './logs.entry' @@ -61,8 +61,8 @@ export function startLogger( ) { let globalContext: Context = {} - internalMonitoring.setExternalContextProvider(() => - lodashMerge({ session_id: session.getId() }, globalContext, getRUMInternalContext()) + internalMonitoring.setExternalContextProvider( + () => deepMerge({ session_id: session.getId() }, globalContext, getRUMInternalContext() as Context) as Context ) const batch = new Batch( @@ -72,7 +72,7 @@ export function startLogger( configuration.maxMessageSize, configuration.flushTimeout, () => - lodashMerge( + deepMerge( { date: new Date().getTime(), session_id: session.getId(), @@ -82,7 +82,7 @@ export function startLogger( }, }, globalContext, - getRUMInternalContext() + getRUMInternalContext() as Context ) as Context ) const handlers = { @@ -141,7 +141,7 @@ export class Logger { @monitored log(message: string, messageContext = {}, status = StatusType.info) { if (this.session.isTracked() && STATUS_PRIORITIES[status] >= STATUS_PRIORITIES[this.level]) { - this.handler({ message, status, ...lodashMerge({}, this.loggerContext, messageContext) }) + this.handler({ message, status, ...(deepMerge({}, this.loggerContext, messageContext) as Context) }) } } @@ -163,7 +163,7 @@ export class Logger { origin: ErrorOrigin.LOGGER, }, } - this.log(message, lodashMerge({}, errorOrigin, messageContext), StatusType.error) + this.log(message, deepMerge({}, errorOrigin, messageContext), StatusType.error) } setContext(context: Context) { diff --git a/packages/logs/src/logs.entry.ts b/packages/logs/src/logs.entry.ts index 167fa283a9..61bb1c2e21 100644 --- a/packages/logs/src/logs.entry.ts +++ b/packages/logs/src/logs.entry.ts @@ -1,5 +1,6 @@ import { areCookiesAuthorized, + assign, checkIsNotLocalFile, commonInit, Context, @@ -11,7 +12,6 @@ import { monitor, UserConfiguration, } from '@datadog/browser-core' -import lodashAssign from 'lodash.assign' import { buildEnv } from './buildEnv' import { HandlerType, Logger, LoggerConfiguration, startLogger, StatusType } from './logger' import { startLoggerSession } from './loggerSession' @@ -94,7 +94,7 @@ datadogLogs.init = monitor((userConfiguration: LogsUserConfiguration) => { const { errorObservable, configuration, internalMonitoring } = commonInit(logsUserConfiguration, buildEnv) const session = startLoggerSession(configuration, areCookiesAuthorized()) const globalApi = startLogger(errorObservable, configuration, session, internalMonitoring) - lodashAssign(datadogLogs, globalApi) + assign(datadogLogs, globalApi) isAlreadyInitialized = true }) diff --git a/packages/rum/package.json b/packages/rum/package.json index 686226153e..72ef5694bd 100644 --- a/packages/rum/package.json +++ b/packages/rum/package.json @@ -14,13 +14,9 @@ }, "dependencies": { "@datadog/browser-core": "1.11.6", - "lodash.assign": "4.2.0", - "lodash.merge": "4.6.2", "tslib": "1.10.0" }, "devDependencies": { - "@types/lodash.assign": "4.2.6", - "@types/lodash.merge": "4.6.6", "@types/sinon": "7.0.13", "sinon": "7.3.2" }, diff --git a/packages/rum/src/rum.entry.ts b/packages/rum/src/rum.entry.ts index ab0d862c3e..daa4379d6b 100644 --- a/packages/rum/src/rum.entry.ts +++ b/packages/rum/src/rum.entry.ts @@ -1,4 +1,5 @@ import { + assign, checkCookiesAuthorized, checkIsNotLocalFile, commonInit, @@ -12,7 +13,6 @@ import { startRequestCollection, UserConfiguration, } from '@datadog/browser-core' -import lodashAssign from 'lodash.assign' import { buildEnv } from './buildEnv' import { startDOMMutationCollection } from './domMutationCollection' @@ -85,7 +85,7 @@ datadogRum.init = monitor((userConfiguration: RumUserConfiguration) => { requestStartObservable.subscribe((startEvent) => lifeCycle.notify(LifeCycleEventType.REQUEST_STARTED, startEvent)) requestCompleteObservable.subscribe((request) => lifeCycle.notify(LifeCycleEventType.REQUEST_COMPLETED, request)) - lodashAssign(datadogRum, globalApi) + assign(datadogRum, globalApi) isAlreadyInitialized = true }) diff --git a/packages/rum/src/rum.ts b/packages/rum/src/rum.ts index 65867075e0..678c87a7d2 100644 --- a/packages/rum/src/rum.ts +++ b/packages/rum/src/rum.ts @@ -3,6 +3,7 @@ import { Configuration, Context, ContextValue, + deepMerge, ErrorContext, ErrorMessage, getTimestamp, @@ -18,7 +19,6 @@ import { ResourceKind, withSnakeCaseKeys, } from '@datadog/browser-core' -import lodashMerge from 'lodash.merge' import { LifeCycle, LifeCycleEventType } from './lifeCycle' import { matchRequestTiming } from './matchRequestTiming' @@ -153,17 +153,18 @@ export function startRum( ): Omit { let globalContext: Context = {} - internalMonitoring.setExternalContextProvider(() => - lodashMerge( - { - application_id: applicationId, - session_id: viewContext.sessionId, - view: { - id: viewContext.id, + internalMonitoring.setExternalContextProvider( + () => + deepMerge( + { + application_id: applicationId, + session_id: viewContext.sessionId, + view: { + id: viewContext.id, + }, }, - }, - globalContext - ) + globalContext + ) as Context ) const batch = startRumBatch( @@ -233,7 +234,7 @@ function startRumBatch( configuration.batchBytesLimit, configuration.maxMessageSize, configuration.flushTimeout, - () => lodashMerge(withSnakeCaseKeys(rumContextProvider()), globalContextProvider()), + () => deepMerge(withSnakeCaseKeys(rumContextProvider()), globalContextProvider()) as Context, beforeUnloadCallback ) return { diff --git a/test/app/yarn.lock b/test/app/yarn.lock index 3ca5ca099d..46db289411 100644 --- a/test/app/yarn.lock +++ b/test/app/yarn.lock @@ -2,27 +2,21 @@ # yarn lockfile v1 -"@datadog/browser-core@1.8.3", "@datadog/browser-core@file:../../packages/core": - version "1.8.3" +"@datadog/browser-core@1.11.4", "@datadog/browser-core@file:../../packages/core": + version "1.11.4" dependencies: - lodash.assign "4.2.0" - lodash.merge "4.6.2" tslib "1.10.0" "@datadog/browser-logs@file:../../packages/logs": - version "1.8.3" + version "1.11.4" dependencies: - "@datadog/browser-core" "1.8.3" - lodash.assign "4.2.0" - lodash.merge "4.6.2" + "@datadog/browser-core" "1.11.4" tslib "1.10.0" "@datadog/browser-rum@file:../../packages/rum": - version "1.8.3" + version "1.11.4" dependencies: - "@datadog/browser-core" "1.8.3" - lodash.assign "4.2.0" - lodash.merge "4.6.2" + "@datadog/browser-core" "1.11.4" tslib "1.10.0" "@webassemblyjs/ast@1.8.5": @@ -1280,16 +1274,6 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" -lodash.assign@4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" - integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= - -lodash.merge@4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" diff --git a/yarn.lock b/yarn.lock index f819d4a48b..dc41eff176 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1080,25 +1080,6 @@ dependencies: "@types/node" "*" -"@types/lodash.assign@4.2.6": - version "4.2.6" - resolved "https://registry.yarnpkg.com/@types/lodash.assign/-/lodash.assign-4.2.6.tgz#e5cc225446c706907893263d5938c6571ad28551" - integrity sha512-SaReADQZqf99FUWZ/gHICOAhLfBvaUmVb9y8xCw7o5WDuqDG0YfN1a+by29eipPcV4FITfPbQMJQiOGAeOb4fw== - dependencies: - "@types/lodash" "*" - -"@types/lodash.merge@4.6.6": - version "4.6.6" - resolved "https://registry.yarnpkg.com/@types/lodash.merge/-/lodash.merge-4.6.6.tgz#b84b403c1d31bc42d51772d1cd5557fa008cd3d6" - integrity sha512-IB90krzMf7YpfgP3u/EvZEdXVvm4e3gJbUvh5ieuI+o+XqiNEt6fCzqNRaiLlPVScLI59RxIGZMQ3+Ko/DJ8vQ== - dependencies: - "@types/lodash" "*" - -"@types/lodash@*": - version "4.14.136" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.136.tgz#413e85089046b865d960c9ff1d400e04c31ab60f" - integrity sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA== - "@types/mime-types@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.0.tgz#9ca52cda363f699c69466c2a6ccdaad913ea7a73" @@ -6122,7 +6103,7 @@ lodash._root@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" integrity sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI= -lodash.assign@4.2.0, lodash.assign@^4.2.0: +lodash.assign@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= @@ -6203,7 +6184,7 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" -lodash.merge@4.6.2, lodash.merge@^4.6.1: +lodash.merge@^4.6.1: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==