Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Allow to modify source_url property in Loaf script attributions #3325

Merged
merged 7 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/rum-core/src/domain/assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ export function startRumAssembly(
...ROOT_MODIFIABLE_FIELD_PATHS,
},
[RumEventType.LONG_TASK]: {
'long_task.scripts[].source_url': 'string',
'long_task.scripts[].invoker': 'string',
...USER_CUSTOMIZABLE_FIELD_PATHS,
...VIEW_MODIFIABLE_FIELD_PATHS,
},
Expand Down
33 changes: 27 additions & 6 deletions packages/rum-core/src/domain/limitModification.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,57 @@ import type { ModifiableFieldPaths } from './limitModification'
import { limitModification } from './limitModification'

describe('limitModification', () => {
let object: unknown

beforeEach(() => {
object = {
foo: { bar: 'bar' },
arr: [{ foo: 'foo' }],
qux: 'qux',
}
})

it('should allow modifications on modifiable field', () => {
const object = { foo: { bar: 'bar' }, qux: 'qux' }
const modifier = (candidate: any) => {
candidate.foo.bar = 'modified1'
candidate.qux = 'modified2'
candidate.arr[0].foo = 'modified3'
}

limitModification(object, { 'foo.bar': 'string', qux: 'string' }, modifier)
limitModification(
object,
{
'foo.bar': 'string',
qux: 'string',
'arr[].foo': 'string',
},
modifier
)

expect(object).toEqual({
foo: { bar: 'modified1' },
qux: 'modified2',
arr: [{ foo: 'modified3' }],
})
})

it('should not allow modifications on non modifiable field', () => {
const object = { foo: { bar: 'bar' }, qux: 'qux' }
const modifier = (candidate: any) => {
candidate.foo.bar = 'modified1'
candidate.qux = 'modified2'
candidate.arr[0].foo = 'modified3'
}

limitModification(object, { 'foo.bar': 'string' }, modifier)

expect(object).toEqual({
foo: { bar: 'modified1' },
arr: [{ foo: 'foo' }],
qux: 'qux',
})
})

it('should allow to add a modifiable fields not present on the original object', () => {
const object = { foo: { bar: 'bar' }, qux: 'qux' }
const modifier = (candidate: any) => {
candidate.foo.bar = 'modified1'
candidate.qux = 'modified2'
Expand All @@ -46,13 +65,13 @@ describe('limitModification', () => {

expect(object as any).toEqual({
foo: { bar: 'modified1' },
arr: [{ foo: 'foo' }],
qux: 'modified2',
qix: 'modified3',
})
})

it('should not allow to add a non modifiable fields not present on the original object', () => {
const object = { foo: { bar: 'bar' }, qux: 'qux' }
const modifier = (candidate: any) => {
candidate.foo.bar = 'modified1'
candidate.qux = 'modified2'
Expand All @@ -63,6 +82,7 @@ describe('limitModification', () => {

expect(object).toEqual({
foo: { bar: 'modified1' },
arr: [{ foo: 'foo' }],
qux: 'modified2',
})
})
Expand Down Expand Up @@ -119,18 +139,19 @@ describe('limitModification', () => {
})

it('should not allow structural change of the object', () => {
const object = { foo: { bar: 'bar' }, qux: 'qux' }
const modifier = (candidate: any) => {
candidate.foo.bar = { qux: 'qux' }
candidate.bar = 'bar'
delete candidate.qux
;(candidate.arr as Array<Record<string, string>>).push({ bar: 'baz' })
}

limitModification(object, { 'foo.bar': 'string', qux: 'string' }, modifier)

expect(object).toEqual({
foo: { bar: 'bar' },
qux: 'qux',
arr: [{ foo: 'foo' }],
})
})

Expand Down
74 changes: 39 additions & 35 deletions packages/rum-core/src/domain/limitModification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import type { Context } from '@datadog/browser-core'
export type ModifiableFieldPaths = Record<string, 'string' | 'object'>

/**
* Current limitation:
* - field path do not support array, 'a.b.c' only
* Allows declaring and enforcing modifications to specific fields of an object.
* Only supports modifying properties of an object (even if nested in an array).
* Does not support array manipulation (adding/removing items).
*/
export function limitModification<T extends Context, Result>(
object: T,
Expand All @@ -14,49 +15,52 @@ export function limitModification<T extends Context, Result>(
): Result | undefined {
const clone = deepClone(object)
const result = modifier(clone)
objectEntries(modifiableFieldPaths).forEach(([fieldPath, fieldType]) => {
const newValue = get(clone, fieldPath)
const newType = getType(newValue)
if (newType === fieldType) {
set(object, fieldPath, sanitize(newValue))
} else if (fieldType === 'object' && (newType === 'undefined' || newType === 'null')) {
set(object, fieldPath, {})
}
})

objectEntries(modifiableFieldPaths).forEach(([fieldPath, fieldType]) =>
// Traverse both object and clone simultaneously up to the path and apply the modification from the clone to the original object when the type is valid
setValueAtPath(object, clone, fieldPath.split(/\.|(?=\[\])/), fieldType)
)

return result
}

function get(object: unknown, path: string) {
let current = object
for (const field of path.split('.')) {
if (!isValidObjectContaining(current, field)) {
return
function setValueAtPath(object: unknown, clone: unknown, pathSegments: string[], fieldType: 'string' | 'object') {
const [field, ...restPathSegments] = pathSegments

if (field === '[]') {
if (Array.isArray(object) && Array.isArray(clone)) {
object.forEach((item, i) => setValueAtPath(item, clone[i], restPathSegments, fieldType))
}
current = current[field]

return
}

if (!isValidObject(object) || !isValidObject(clone)) {
return
}
return current

if (restPathSegments.length > 0) {
return setValueAtPath(object[field], clone[field], restPathSegments, fieldType)
}

setNestedValue(object, field, clone[field], fieldType)
}

function set(object: unknown, path: string, value: unknown) {
let current = object
const fields = path.split('.')
for (let i = 0; i < fields.length; i += 1) {
const field = fields[i]
if (!isValidObject(current)) {
return
}
if (i !== fields.length - 1) {
current = current[field]
} else {
current[field] = value
}
function setNestedValue(
object: Record<string, unknown>,
field: string,
value: unknown,
fieldType: 'string' | 'object'
) {
const newType = getType(value)

if (newType === fieldType) {
object[field] = sanitize(value)
} else if (fieldType === 'object' && (newType === 'undefined' || newType === 'null')) {
object[field] = {}
}
}

function isValidObject(object: unknown): object is Record<string, unknown> {
return getType(object) === 'object'
}

function isValidObjectContaining(object: unknown, field: string): object is Record<string, unknown> {
return isValidObject(object) && Object.prototype.hasOwnProperty.call(object, field)
}
4 changes: 2 additions & 2 deletions packages/rum-core/src/rumEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ export type RumLongTaskEvent = CommonProperties &
/**
* The script resource name where available (or empty if not found)
*/
readonly source_url?: string
source_url?: string
/**
* The script function name where available (or empty if not found)
*/
Expand All @@ -528,7 +528,7 @@ export type RumLongTaskEvent = CommonProperties &
/**
* Information about the invoker of the script
*/
readonly invoker?: string
invoker?: string
/**
* Type of the invoker of the script
*/
Expand Down