Skip to content

Commit 571386f

Browse files
authored
Add text content, clean up innerText and textContent, make Button Clicks more reliable (#1151)
1 parent f8cdce1 commit 571386f

File tree

9 files changed

+151
-18
lines changed

9 files changed

+151
-18
lines changed

.changeset/weak-mice-judge.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@segment/analytics-signals': patch
3+
---
4+
5+
- Clean up up innerText AND textContent artifacts to make easier to parse.
6+
- Add textContent field
7+
- Make button Clicks more reliable

packages/signals/signals-example/src/components/ComplexForm.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ const ComplexForm = () => {
5757
</div>
5858
<button type="submit">Submit</button>
5959
</form>
60+
<button>
61+
<div>
62+
Other Example Button with <h1>Nested Text</h1>
63+
</div>
64+
</button>
6065
</div>
6166
)
6267
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ test('interaction signals', async () => {
123123
labels: [],
124124
name: '',
125125
nodeName: 'BUTTON',
126-
nodeValue: null,
127126
tagName: 'BUTTON',
128127
title: '',
129128
type: 'submit',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { test, expect } from '@playwright/test'
2+
import type { SegmentEvent } from '@segment/analytics-next'
3+
import { IndexPage } from './index-page'
4+
5+
const indexPage = new IndexPage()
6+
7+
const basicEdgeFn = `
8+
// this is a process signal function
9+
const processSignal = (signal) => {}`
10+
11+
test.beforeEach(async ({ page }) => {
12+
await indexPage.loadAndWait(page, basicEdgeFn)
13+
})
14+
15+
test('button click (complex, with nested items)', async () => {
16+
/**
17+
* Click a button with nested text, ensure that that correct text shows up
18+
*/
19+
await Promise.all([
20+
indexPage.clickComplexButton(),
21+
indexPage.waitForSignalsApiFlush(),
22+
])
23+
24+
const signalsReqJSON = indexPage.lastSignalsApiReq.postDataJSON()
25+
const interactionSignals = signalsReqJSON.batch.filter(
26+
(el: SegmentEvent) => el.properties!.type === 'interaction'
27+
)
28+
expect(interactionSignals).toHaveLength(1)
29+
const data = {
30+
eventType: 'click',
31+
target: {
32+
attributes: {
33+
id: 'complex-button',
34+
},
35+
classList: [],
36+
id: 'complex-button',
37+
labels: [],
38+
name: '',
39+
nodeName: 'BUTTON',
40+
tagName: 'BUTTON',
41+
title: '',
42+
type: 'submit',
43+
innerText: expect.any(String),
44+
textContent: expect.stringContaining(
45+
'Other Example Button with Nested Text'
46+
),
47+
value: '',
48+
},
49+
}
50+
51+
expect(interactionSignals[0]).toMatchObject({
52+
event: 'Segment Signal Generated',
53+
type: 'track',
54+
properties: {
55+
type: 'interaction',
56+
data,
57+
},
58+
})
59+
})

packages/signals/signals-integration-tests/src/tests/signals-vanilla/index-page.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,8 @@ export class IndexPage extends BasePage {
3636
async clickButton() {
3737
return this.page.click('#some-button')
3838
}
39+
40+
async clickComplexButton() {
41+
return this.page.click('#complex-button')
42+
}
3943
}

packages/signals/signals-integration-tests/src/tests/signals-vanilla/index.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99

1010
<body>
1111
<button id="some-button">Click me</button>
12+
<button id="complex-button">
13+
<img id="some-image" src="https://via.placeholder.com/150" alt="Placeholder Image">
14+
<div>
15+
Other Example Button with <h1>Nested Text</h1>
16+
</div>
17+
</button>
18+
1219
<form>
1320
<label for="name">Name:</label>
1421
<input type="text" id="name" name="name"><br><br>
@@ -19,5 +26,4 @@
1926
<input type="submit" value="Submit">
2027
</form>
2128
</body>
22-
2329
</html>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { cleanText } from '../dom-gen'
2+
3+
describe(cleanText, () => {
4+
test('should remove newline characters', () => {
5+
const input = 'Hello\nWorld\n'
6+
const expected = 'Hello World'
7+
expect(cleanText(input)).toBe(expected)
8+
})
9+
10+
test('should remove tab characters', () => {
11+
const input = 'Hello\tWorld\t'
12+
const expected = 'Hello World'
13+
expect(cleanText(input)).toBe(expected)
14+
})
15+
16+
test('should replace multiple spaces with a single space', () => {
17+
const input = 'Hello World'
18+
const expected = 'Hello World'
19+
expect(cleanText(input)).toBe(expected)
20+
})
21+
22+
test('should replace non-breaking spaces with regular spaces', () => {
23+
const input = 'Hello\u00A0World'
24+
const expected = 'Hello World'
25+
expect(cleanText(input)).toBe(expected)
26+
})
27+
28+
test('should trim leading and trailing spaces', () => {
29+
const input = ' Hello World '
30+
const expected = 'Hello World'
31+
expect(cleanText(input)).toBe(expected)
32+
})
33+
34+
test('should handle a combination of special characters', () => {
35+
const input = ' \n\tHello\u00A0 World\n\t '
36+
const expected = 'Hello World'
37+
expect(cleanText(input)).toBe(expected)
38+
})
39+
40+
test('should return an empty string if input is empty', () => {
41+
const input = ''
42+
const expected = ''
43+
expect(cleanText(input)).toBe(expected)
44+
})
45+
46+
test('should return the same string if there are no special characters', () => {
47+
const input = 'Hello World'
48+
const expected = 'Hello World'
49+
expect(cleanText(input)).toBe(expected)
50+
})
51+
})

packages/signals/signals/src/core/signal-generators/dom-gen.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ const parseNodeMap = (nodeMap: NamedNodeMap): Record<string, unknown> => {
2323
}, {} as Record<string, unknown>)
2424
}
2525

26+
export const cleanText = (str: string): string => {
27+
return str
28+
.replace(/[\r\n\t]+/g, ' ') // Replace newlines and tabs with a space
29+
.replace(/\s\s+/g, ' ') // Replace multiple spaces with a single space
30+
.replace(/\u00A0/g, ' ') // Replace non-breaking spaces with a regular space
31+
.trim() // Trim leading and trailing spaces
32+
}
33+
2634
const parseElement = (el: HTMLElement) => {
2735
const base = {
2836
// adding a bunch of fields that are not on _all_ elements, but are on enough that it's useful to have them here.
@@ -32,11 +40,12 @@ const parseElement = (el: HTMLElement) => {
3240
labels: parseLabels((el as HTMLInputElement).labels),
3341
name: (el as HTMLInputElement).name,
3442
nodeName: el.nodeName,
35-
nodeValue: el.nodeValue,
3643
tagName: el.tagName,
3744
title: el.title,
3845
type: (el as HTMLInputElement).type,
3946
value: (el as HTMLInputElement).value,
47+
textContent: el.textContent && cleanText(el.textContent),
48+
innerText: el.innerText && cleanText(el.innerText),
4049
}
4150

4251
if (el instanceof HTMLSelectElement) {
@@ -67,11 +76,6 @@ const parseElement = (el: HTMLElement) => {
6776
src: el.src,
6877
volume: el.volume,
6978
}
70-
} else if (el instanceof HTMLButtonElement) {
71-
return {
72-
...base,
73-
innerText: el.innerText,
74-
}
7579
}
7680
return base
7781
}
@@ -81,12 +85,14 @@ export class ClickSignalsGenerator implements SignalGenerator {
8185

8286
register(emitter: SignalEmitter) {
8387
const handleClick = (ev: MouseEvent) => {
84-
const target = (ev.target as HTMLElement) ?? {}
85-
if (this.isClickableElement(target)) {
88+
const target = ev.target as HTMLElement | null
89+
if (!target) return
90+
const el = this.getClosestClickableElement(target)
91+
if (el) {
8692
emitter.emit(
8793
createInteractionSignal({
8894
eventType: 'click',
89-
target: parseElement(target),
95+
target: parseElement(el),
9096
})
9197
)
9298
}
@@ -95,12 +101,9 @@ export class ClickSignalsGenerator implements SignalGenerator {
95101
return () => document.removeEventListener('click', handleClick)
96102
}
97103

98-
private isClickableElement(el: HTMLElement): boolean {
99-
return (
100-
el instanceof HTMLAnchorElement ||
101-
el instanceof HTMLButtonElement ||
102-
['button', 'link'].includes(el.getAttribute('role') ?? '')
103-
)
104+
private getClosestClickableElement(el: HTMLElement): HTMLElement | null {
105+
// if you click on a nested element, we want to get the closest clickable ancestor. Useful for things like buttons with nested text or images
106+
return el.closest<HTMLElement>('button, a, [role="button"], [role="link"]')
104107
}
105108
}
106109

packages/signals/signals/src/types/signals.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export type InteractionData = ClickData | SubmitData | ChangeData
1818
interface SerializedTarget {
1919
// nodeName: Node['nodeName']
2020
// textContent: Node['textContent']
21-
// nodeValue: Node['nodeValue']
2221
// nodeType: Node['nodeType']
2322
[key: string]: any
2423
}

0 commit comments

Comments
 (0)