Skip to content

Commit 230001c

Browse files
committed
Add support for mutation signals
1 parent 5caf4d6 commit 230001c

File tree

28 files changed

+1110
-125
lines changed

28 files changed

+1110
-125
lines changed

.vscode/launch.json

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,10 @@
66
"request": "launch",
77
"name": "Jest Current File",
88
"program": "${workspaceFolder}/node_modules/.bin/jest",
9-
"args": [
10-
"--testTimeout=100000",
11-
"--findRelatedTests",
12-
"${relativeFile}"
13-
],
9+
"args": ["--testTimeout=100000", "--findRelatedTests", "${relativeFile}"],
1410
"console": "integratedTerminal",
1511
"internalConsoleOptions": "neverOpen",
16-
"skipFiles": [
17-
"<node_internals>/**"
18-
]
12+
"skipFiles": ["<node_internals>/**"]
1913
},
2014
{
2115
"type": "node",
@@ -30,9 +24,17 @@
3024
],
3125
"console": "integratedTerminal",
3226
"internalConsoleOptions": "neverOpen",
33-
"skipFiles": [
34-
"<node_internals>/**"
35-
]
27+
"skipFiles": ["<node_internals>/**"]
28+
},
29+
{
30+
"name": "Run Jest Tests for Current Package",
31+
"type": "node",
32+
"request": "launch",
33+
"program": "${workspaceFolder}/node_modules/.bin/jest",
34+
"args": ["--testTimeout=100000"],
35+
"console": "integratedTerminal",
36+
"internalConsoleOptions": "neverOpen",
37+
"cwd": "${fileDirname}"
3638
},
3739
{
3840
"type": "node",
@@ -47,9 +49,7 @@
4749
"console": "integratedTerminal",
4850
"internalConsoleOptions": "neverOpen",
4951
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
50-
"skipFiles": [
51-
"<node_internals>/**"
52-
]
52+
"skipFiles": ["<node_internals>/**"]
5353
},
5454
{
5555
"type": "node",
@@ -64,9 +64,7 @@
6464
"console": "integratedTerminal",
6565
"internalConsoleOptions": "neverOpen",
6666
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
67-
"skipFiles": [
68-
"<node_internals>/**"
69-
]
67+
"skipFiles": ["<node_internals>/**"]
7068
},
7169
{
7270
"name": "ts-node Current File",

packages/signals/signals-integration-tests/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"scripts": {
99
".": "yarn run -T turbo run --filter=@internal/signals-integration-tests...",
1010
"build": "webpack",
11-
"test": "playwright test",
11+
"test": "playwright test src/tests/signals-vanilla",
12+
"test:perf": "playwright test src/tests/performance",
1213
"watch": "webpack -w",
1314
"lint": "yarn concurrently 'yarn:eslint .' 'yarn:tsc --noEmit'",
1415
"concurrently": "yarn run -T concurrently",

packages/signals/signals-integration-tests/playwright.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@ const config: PlaywrightTestConfig = {
3131
/* Opt out of parallel tests on CI. */
3232
workers: process.env.CI ? 1 : undefined,
3333
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
34-
reporter: 'html',
34+
reporter: [['html', { open: 'never' }]],
3535
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
3636
use: {
3737
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
3838
trace: 'on',
39+
launchOptions: {
40+
args: ['--enable-precise-memory-info', '--js-flags=--expose-gc'],
41+
},
3942
},
4043

4144
/* Configure projects for major browsers */
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { BasePage } from '../../helpers/base-page-object'
2+
3+
export class IndexPage extends BasePage {
4+
constructor() {
5+
super(`/performance/index.html`)
6+
}
7+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<script src="../../../dist/signals-vanilla.bundle.js"></script>
6+
<!-- Dummy favicon -->
7+
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
8+
</head>
9+
10+
<body>
11+
<div id="test-container"></div>
12+
</body>
13+
14+
</html>
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { test, expect, Page } from '@playwright/test'
2+
import { IndexPage } from './index-page'
3+
import { sleep } from '@segment/analytics-core'
4+
5+
declare global {
6+
interface Window {
7+
gc: () => void // chrome specific
8+
}
9+
interface Performance {
10+
memory: {
11+
usedJSHeapSize: number
12+
totalJSHeapSize: number
13+
}
14+
}
15+
}
16+
17+
const basicEdgeFn = `
18+
// this is a process signal function
19+
const processSignal = (signal) => {
20+
if (signal.type === 'interaction') {
21+
const eventName = signal.data.eventType + ' ' + '[' + signal.type + ']'
22+
analytics.track(eventName, signal.data)
23+
}
24+
}`
25+
26+
const checkForMemoryLeak = async (page: Page) => {
27+
const ALLOWED_GROWTH = 1.1
28+
const getMemoryUsage = (): Promise<number> =>
29+
page.evaluate(() => {
30+
return performance.memory.usedJSHeapSize
31+
})
32+
33+
const firstMemory = await getMemoryUsage()
34+
35+
// add nodes
36+
await page.evaluate(() => {
37+
const target = document.getElementById('test-container')!
38+
const NODE_COUNT = 2000
39+
for (let i = 0; i < NODE_COUNT; i++) {
40+
const newNode = document.createElement('input')
41+
newNode.type = 'text'
42+
newNode.value = Math.random().toString()
43+
target.appendChild(newNode)
44+
}
45+
})
46+
47+
// remove all the nodes
48+
await page.evaluate(() => {
49+
const target = document.getElementById('test-container')!
50+
while (target.firstChild) {
51+
target.removeChild(target.firstChild)
52+
}
53+
})
54+
const inputNodeLength = await page.evaluate(
55+
() => document.querySelectorAll('input').length
56+
)
57+
expect(inputNodeLength).toBe(0)
58+
59+
await page.evaluate(() => {
60+
// force run garbage collection: --js-flags="--expose-gc" is required
61+
window.gc()
62+
})
63+
64+
await sleep(500) // may not be needed, but just in case.
65+
66+
const lastMemory = await getMemoryUsage() // Allow some fluctuation, but fail if there's a significant memory increase
67+
const report = `initial: ${firstMemory}, final: ${lastMemory}, allowed growth: ${ALLOWED_GROWTH}`
68+
if (lastMemory > firstMemory * ALLOWED_GROWTH) {
69+
throw new Error(`Memory leak detected! ${report}`)
70+
} else {
71+
console.log('Memory leak test passed!', `initial: ${report}`)
72+
}
73+
}
74+
75+
test('memory leak test scaffold works', async ({ page }) => {
76+
const htmlContent = `
77+
<!DOCTYPE html>
78+
<html>
79+
<head>
80+
<title>Test Page</title>
81+
</head>
82+
<body>
83+
<div id="test-container"></div>
84+
</body>
85+
</html>
86+
`
87+
await page.setContent(htmlContent)
88+
await page.waitForLoadState('networkidle')
89+
await checkForMemoryLeak(page)
90+
})
91+
92+
test('memory leak', async ({ page }) => {
93+
const indexPage = await new IndexPage().loadAndWait(page, basicEdgeFn)
94+
await indexPage.waitForSignalsApiFlush()
95+
await page.waitForLoadState('networkidle')
96+
await checkForMemoryLeak(page)
97+
})

packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { test, expect } from '@playwright/test'
22
import { IndexPage } from './index-page'
3+
import { waitForCondition } from '../../helpers/playwright-utils'
34

45
const indexPage = new IndexPage()
56

@@ -56,6 +57,10 @@ test('debug ingestion disabled and sample rate 1 -> will send the signal', async
5657
sampleRate: 1,
5758
}
5859
)
59-
await indexPage.fillNameInput('John Doe')
60-
expect(indexPage.signalsAPI.getEvents('interaction')).toHaveLength(1)
60+
await Promise.all([
61+
indexPage.fillNameInput('John Doe'),
62+
waitForCondition(
63+
() => indexPage.signalsAPI.getEvents('interaction').length > 0
64+
),
65+
])
6166
})

packages/signals/signals-runtime/src/web/web-signals-types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ type SubmitData = {
2525
target: SerializedTarget
2626
}
2727

28-
type ChangeData = {
28+
export type ChangeData = {
2929
eventType: 'change'
30-
[key: string]: unknown
30+
target: SerializedTarget
31+
listener: 'contenteditable' | 'onchange' | 'mutation'
32+
change: JSONValue
3133
}
3234

3335
export type InteractionSignal = RawSignal<'interaction', InteractionData>

packages/signals/signals/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"build:bundle": "NODE_ENV=production yarn run webpack",
3333
"workerbox": "node scripts/build-workerbox.js",
3434
"assert-generated": "sh scripts/assert-workerbox-built.sh",
35-
"watch": "yarn concurrently 'yarn build:bundle --watch' 'yarn build:esm --watch'",
35+
"watch": "rm -rf dist && yarn concurrently 'yarn build:bundle --watch' 'yarn build:esm --watch'",
3636
"version": "sh scripts/version.sh",
3737
"watch:test": "yarn test --watch",
3838
"tsc": "yarn run -T tsc",

packages/signals/signals/src/core/buffer/__tests__/buffer.test.ts

Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ import { range } from '../../../test-helpers/range'
33
import { createInteractionSignal } from '../../../types/factories'
44
import { getSignalBuffer, SignalBuffer } from '../index'
55

6+
const createMockSignal = () =>
7+
createInteractionSignal({
8+
eventType: 'submit',
9+
target: { innerText: Math.random().toString() },
10+
})
11+
612
describe(getSignalBuffer, () => {
713
let buffer: SignalBuffer
814
beforeEach(async () => {
@@ -28,13 +34,7 @@ describe(getSignalBuffer, () => {
2834
})
2935

3036
it('should delete older signals when maxBufferSize is exceeded', async () => {
31-
const signals = range(15).map((_, idx) =>
32-
createInteractionSignal({
33-
idx: idx,
34-
eventType: 'change',
35-
target: {},
36-
})
37-
)
37+
const signals = range(15).map(() => createMockSignal())
3838

3939
for (const signal of signals) {
4040
await buffer.add(signal)
@@ -46,13 +46,7 @@ describe(getSignalBuffer, () => {
4646
})
4747

4848
it('should delete older signals on initialize if current number exceeds maxBufferSize', async () => {
49-
const signals = range(15).map((_, idx) =>
50-
createInteractionSignal({
51-
idx: idx,
52-
eventType: 'change',
53-
target: {},
54-
})
55-
)
49+
const signals = range(15).map((_) => createMockSignal())
5650

5751
for (const signal of signals) {
5852
await buffer.add(signal)
@@ -69,10 +63,7 @@ describe(getSignalBuffer, () => {
6963
})
7064

7165
it('should clear signal buffer if there is a new session according to session storage', async () => {
72-
const mockSignal = createInteractionSignal({
73-
eventType: 'submit',
74-
target: {},
75-
})
66+
const mockSignal = createMockSignal()
7667
await buffer.add(mockSignal)
7768
await expect(buffer.getAll()).resolves.toEqual([mockSignal])
7869

@@ -92,24 +83,15 @@ describe(getSignalBuffer, () => {
9283
})
9384

9485
it('should add and clear', async () => {
95-
const mockSignal = createInteractionSignal({
96-
eventType: 'submit',
97-
target: {},
98-
})
86+
const mockSignal = createMockSignal()
9987
await buffer.add(mockSignal)
10088
await expect(buffer.getAll()).resolves.toEqual([mockSignal])
10189
await buffer.clear()
10290
await expect(buffer.getAll()).resolves.toHaveLength(0)
10391
})
10492

10593
it('should delete older signals when maxBufferSize is exceeded', async () => {
106-
const signals = range(15).map((_, idx) =>
107-
createInteractionSignal({
108-
idx: idx,
109-
eventType: 'change',
110-
target: {},
111-
})
112-
)
94+
const signals = range(15).map(() => createMockSignal())
11395

11496
for (const signal of signals) {
11597
await buffer.add(signal)

0 commit comments

Comments
 (0)