@playpilot/tpi 8.9.3 → 8.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editorial.mount.js +7 -7
- package/dist/link-injections.js +1 -1
- package/dist/mount.js +5 -5
- package/events.md +2 -0
- package/package.json +1 -1
- package/src/lib/enums/TrackingEvent.ts +2 -0
- package/src/routes/components/Explore/ExploreLayout.svelte +5 -0
- package/src/routes/components/TrackAnyClick.svelte +38 -0
- package/src/routes/components/TrackScrollDistance.svelte +38 -0
- package/src/tests/routes/components/TrackAnyClick.test.js +107 -0
package/events.md
CHANGED
|
@@ -122,3 +122,5 @@ Event | Action | Info | Payload
|
|
|
122
122
|
`venus_title_rail_expand_click` | _Fires any time a title is expanded in a rail directly via a click_ | Does not fire when a title opens automatically on load or navigate | `Title`, `rail` (heading key of the rail)
|
|
123
123
|
`venus_title_rail_set_index` | _Fires when navigating in the titles rail modal, either via arrows, swipe, or clicking titles_ | | `Title`, `index` (index of the new position of the slider)
|
|
124
124
|
`venus_navigate` | _Fires when navigating on the explore page_ | Does not fire on initial load | `route` (the key of the given route)
|
|
125
|
+
`venus_any_click` | _Fires on any click anywhere_ | | `selector` (parent > child selector of the clicked element), `text` (direct text content of the clicked element, limited to 30 characters)
|
|
126
|
+
`venus_scroll_distance` | _Fires on increments of scroll distance_ | 25%, 50%, 75%, 100% | `scroll_percentage` (percentage of the explore element the user has seen)
|
package/package.json
CHANGED
|
@@ -77,6 +77,8 @@ export const TrackingEvent = {
|
|
|
77
77
|
ExploreTitleRailExpandClick: 'venus_title_rail_expand_click',
|
|
78
78
|
ExploreTitleRailSetIndex: 'venus_title_rail_set_index',
|
|
79
79
|
ExploreNavigate: 'venus_navigate',
|
|
80
|
+
ExploreAnyClick: 'venus_any_click',
|
|
81
|
+
ExploreScrollDistance: 'venus_scroll_distance',
|
|
80
82
|
} as const
|
|
81
83
|
|
|
82
84
|
export const MetaEvent = {
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import { onMount, type Snippet } from 'svelte'
|
|
10
10
|
import Search from './Filter/Search.svelte'
|
|
11
11
|
import Filter from './Filter/Filter.svelte'
|
|
12
|
+
import TrackAnyClick from '../TrackAnyClick.svelte'
|
|
13
|
+
import TrackScrollDistance from '../TrackScrollDistance.svelte'
|
|
12
14
|
|
|
13
15
|
interface Props {
|
|
14
16
|
navigate?: (key: string) => void
|
|
@@ -70,6 +72,9 @@
|
|
|
70
72
|
{@render children()}
|
|
71
73
|
</div>
|
|
72
74
|
|
|
75
|
+
<TrackAnyClick />
|
|
76
|
+
<TrackScrollDistance {element} />
|
|
77
|
+
|
|
73
78
|
<style lang="scss">
|
|
74
79
|
.explore {
|
|
75
80
|
background: theme(explore-background, light);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
3
|
+
import { track } from '$lib/tracking'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
trackingEvent?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { trackingEvent = TrackingEvent.ExploreAnyClick }: Props = $props()
|
|
10
|
+
|
|
11
|
+
function onclick(event: MouseEvent): void {
|
|
12
|
+
const target = event.target as Element
|
|
13
|
+
const parent = target.parentElement as Element
|
|
14
|
+
|
|
15
|
+
if (target === document.body) return
|
|
16
|
+
|
|
17
|
+
const parentSelector = `${parent?.nodeName.toLowerCase()}${getClasslistAsString(parent)}`
|
|
18
|
+
const targetSelector = `${target?.nodeName.toLowerCase()}${getClasslistAsString(target)}`
|
|
19
|
+
const selector = `${parentSelector} > ${targetSelector}`
|
|
20
|
+
|
|
21
|
+
const hasDirectTextContent = Array.from(target.childNodes).some(node => node.nodeName === '#text' && !!node.nodeValue?.trim())
|
|
22
|
+
const text = hasDirectTextContent ? target.textContent.slice(0, 30) : ''
|
|
23
|
+
|
|
24
|
+
track(trackingEvent, null, { selector, text })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getClasslistAsString(element: Element): string {
|
|
28
|
+
if (!element) return ''
|
|
29
|
+
|
|
30
|
+
const classnames = Array.from(element.classList).filter(classname => !classname.startsWith('s-'))
|
|
31
|
+
|
|
32
|
+
if (!classnames.length) return ''
|
|
33
|
+
|
|
34
|
+
return '.' + classnames.join('.')
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<svelte:window {onclick} />
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
3
|
+
import { track } from '$lib/tracking'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
element: Element
|
|
7
|
+
trackingEvent?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { element, trackingEvent = TrackingEvent.ExploreScrollDistance }: Props = $props()
|
|
11
|
+
|
|
12
|
+
const visibilityTrackingPercentages: Record<string, boolean> = {
|
|
13
|
+
'25': false,
|
|
14
|
+
'50': false,
|
|
15
|
+
'75': false,
|
|
16
|
+
'100': false,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function onscroll(): void {
|
|
20
|
+
const scrollDistance = window.scrollY
|
|
21
|
+
const windowHeight = window.innerHeight
|
|
22
|
+
const elementHeight = element.clientHeight
|
|
23
|
+
const elementBottomDistance = elementHeight + scrollDistance + element.getBoundingClientRect().top
|
|
24
|
+
|
|
25
|
+
const totalVisibilityPercentage = 100 / elementBottomDistance * (scrollDistance + windowHeight)
|
|
26
|
+
|
|
27
|
+
for (const percentage in visibilityTrackingPercentages) {
|
|
28
|
+
if (visibilityTrackingPercentages[percentage]) continue
|
|
29
|
+
if (totalVisibilityPercentage < parseInt(percentage)) continue
|
|
30
|
+
|
|
31
|
+
visibilityTrackingPercentages[percentage] = true
|
|
32
|
+
|
|
33
|
+
track(trackingEvent, null, { scroll_percentage: percentage + '%' })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<svelte:window {onscroll} />
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
|
3
|
+
import { track } from '$lib/tracking'
|
|
4
|
+
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
5
|
+
import ClickTracking from '../../../routes/components/TrackAnyClick.svelte'
|
|
6
|
+
|
|
7
|
+
vi.mock('$lib/tracking', () => ({
|
|
8
|
+
track: vi.fn(),
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
describe('TrackAnyClick.svelte', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
document.body.innerHTML = ''
|
|
14
|
+
vi.resetAllMocks()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('Should call track with the correct selector and text content on click', async () => {
|
|
18
|
+
document.body.innerHTML = '<button class="some-button">Some button</button>'
|
|
19
|
+
|
|
20
|
+
const { getByRole } = render(ClickTracking)
|
|
21
|
+
|
|
22
|
+
await fireEvent.click(getByRole('button'))
|
|
23
|
+
|
|
24
|
+
expect(track).toHaveBeenCalledWith(TrackingEvent.ExploreAnyClick, null, { selector: 'body > button.some-button', text: 'Some button' })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('Should not call track when body is clicked', async () => {
|
|
28
|
+
// @ts-ignore
|
|
29
|
+
await fireEvent.click(document.querySelector('body'))
|
|
30
|
+
|
|
31
|
+
expect(track).not.toHaveBeenCalled()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('Should limit text to 30 characters', async () => {
|
|
35
|
+
document.body.innerHTML = '<button class="some-button">This is a very long text that exceeds thirty characters</button>'
|
|
36
|
+
|
|
37
|
+
const { getByRole } = render(ClickTracking)
|
|
38
|
+
|
|
39
|
+
await fireEvent.click(getByRole('button'))
|
|
40
|
+
|
|
41
|
+
expect(track).toHaveBeenCalledWith(
|
|
42
|
+
TrackingEvent.ExploreAnyClick,
|
|
43
|
+
null,
|
|
44
|
+
expect.objectContaining({
|
|
45
|
+
text: 'This is a very long text that ',
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('Should not include text when element has no direct text node', async () => {
|
|
51
|
+
document.body.innerHTML = '<button><span>Some text</span></button>'
|
|
52
|
+
|
|
53
|
+
const { getByRole } = render(ClickTracking)
|
|
54
|
+
|
|
55
|
+
await fireEvent.click(getByRole('button'))
|
|
56
|
+
|
|
57
|
+
expect(track).toHaveBeenCalledWith(
|
|
58
|
+
TrackingEvent.ExploreAnyClick,
|
|
59
|
+
null,
|
|
60
|
+
expect.objectContaining({
|
|
61
|
+
text: '',
|
|
62
|
+
}),
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('Should include class names in the selector', async () => {
|
|
67
|
+
document.body.innerHTML = '<div class="some element"><button class="some button">Some text</button></div>'
|
|
68
|
+
|
|
69
|
+
const { getByRole } = render(ClickTracking)
|
|
70
|
+
|
|
71
|
+
await fireEvent.click(getByRole('button'))
|
|
72
|
+
|
|
73
|
+
expect(track).toHaveBeenCalledWith(
|
|
74
|
+
TrackingEvent.ExploreAnyClick,
|
|
75
|
+
null,
|
|
76
|
+
expect.objectContaining({
|
|
77
|
+
selector: 'div.some.element > button.some.button',
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('Should filter out svelte-generated s- classes from selector', async () => {
|
|
83
|
+
document.body.innerHTML = '<div class="some element s-abc"><button class="some button s-123">Some text</button></div>'
|
|
84
|
+
|
|
85
|
+
const { getByRole } = render(ClickTracking)
|
|
86
|
+
|
|
87
|
+
await fireEvent.click(getByRole('button'))
|
|
88
|
+
|
|
89
|
+
expect(track).toHaveBeenCalledWith(
|
|
90
|
+
TrackingEvent.ExploreAnyClick,
|
|
91
|
+
null,
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
selector: 'div.some.element > button.some.button',
|
|
94
|
+
}),
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('Should build selector without classes when element has none', async () => {
|
|
99
|
+
document.body.innerHTML = '<section><p>Some text</p></section>'
|
|
100
|
+
|
|
101
|
+
const { getByRole } = render(ClickTracking)
|
|
102
|
+
|
|
103
|
+
await fireEvent.click(getByRole('paragraph'))
|
|
104
|
+
|
|
105
|
+
expect(track).toHaveBeenCalledWith(TrackingEvent.ExploreAnyClick, null, expect.objectContaining({ selector: 'section > p' }))
|
|
106
|
+
})
|
|
107
|
+
})
|