Skip to content

Commit 55a48a0

Browse files
authored
Refactor page enrichment plugin (#838)
1 parent 65a3176 commit 55a48a0

File tree

5 files changed

+170
-59
lines changed

5 files changed

+170
-59
lines changed

.changeset/quiet-islands-kneel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@segment/analytics-next': patch
3+
---
4+
5+
Refactor page enrichment to only call page defaults once, and simplify logic
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { pick } from '../pick'
2+
3+
describe(pick, () => {
4+
it.each([
5+
{
6+
obj: { a: 1, b: '2', c: 3 },
7+
keys: ['a', 'c'],
8+
expected: { a: 1, c: 3 },
9+
},
10+
{
11+
obj: { a: 1, '0': false, c: 3 },
12+
keys: ['a', '0'],
13+
expected: { a: 1, '0': false },
14+
},
15+
{ obj: { a: 1, b: '2', c: 3 }, keys: [], expected: {} },
16+
{
17+
obj: { a: undefined, b: null, c: 1 },
18+
keys: ['a', 'b'],
19+
expected: { a: undefined, b: null },
20+
},
21+
])('.pick($obj, $keys)', ({ obj, keys, expected }) => {
22+
expect(pick(obj, keys as (keyof typeof obj)[])).toEqual(expected)
23+
})
24+
it('does not mutate object reference', () => {
25+
const e = {
26+
obj: { a: 1, '0': false, c: 3 },
27+
keys: ['a', '0'] as const,
28+
expected: { a: 1, '0': false },
29+
}
30+
const ogObj = { ...e.obj }
31+
pick(e.obj, e.keys)
32+
expect(e.obj).toEqual(ogObj)
33+
})
34+
})

packages/browser/src/lib/pick.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* @example
3+
* pick({ 'a': 1, 'b': '2', 'c': 3 }, ['a', 'c'])
4+
* => { 'a': 1, 'c': 3 }
5+
*/
6+
export const pick = <T, K extends keyof T>(
7+
object: T,
8+
keys: readonly K[]
9+
): Pick<T, K> =>
10+
Object.assign(
11+
{},
12+
...keys.map((key) => {
13+
if (object && Object.prototype.hasOwnProperty.call(object, key)) {
14+
return { [key]: object[key] }
15+
}
16+
})
17+
)

packages/browser/src/plugins/page-enrichment/__tests__/index.test.ts

Lines changed: 92 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
import { Analytics } from '../../../core/analytics'
22
import { pageEnrichment, pageDefaults } from '..'
3+
import { pick } from '../../../lib/pick'
34

45
let ajs: Analytics
56

7+
const helpers = {
8+
get pageProps() {
9+
return {
10+
url: 'http://foo.com/bar?foo=hello_world',
11+
path: '/bar',
12+
search: '?foo=hello_world',
13+
referrer: 'http://google.com',
14+
title: 'Hello World',
15+
}
16+
},
17+
}
18+
619
describe('Page Enrichment', () => {
720
beforeEach(async () => {
821
ajs = new Analytics({
@@ -43,6 +56,49 @@ describe('Page Enrichment', () => {
4356
`)
4457
})
4558

59+
describe('event.properties override behavior', () => {
60+
test('special page properties in event.properties (url, referrer, etc) are copied to context.page', async () => {
61+
const eventProps = { ...helpers.pageProps }
62+
;(eventProps as any)['should_not_show_up'] = 'hello'
63+
const ctx = await ajs.track('My Event', eventProps)
64+
const page = ctx.event.context!.page
65+
expect(page).toEqual(
66+
pick(eventProps, ['url', 'path', 'referrer', 'search', 'title'])
67+
)
68+
})
69+
70+
test('event page properties should not be mutated', async () => {
71+
const eventProps = { ...helpers.pageProps }
72+
const ctx = await ajs.track('My Event', eventProps)
73+
const page = ctx.event.context!.page
74+
expect(page).toEqual(eventProps)
75+
})
76+
77+
test('page properties should have defaults', async () => {
78+
const eventProps = pick(helpers.pageProps, ['path', 'referrer'])
79+
const ctx = await ajs.track('My Event', eventProps)
80+
const page = ctx.event.context!.page
81+
expect(page).toEqual({
82+
...eventProps,
83+
url: 'http://localhost/',
84+
search: '',
85+
title: '',
86+
})
87+
})
88+
89+
test('undefined / null / empty string properties on event get overridden as usual', async () => {
90+
const eventProps = { ...helpers.pageProps }
91+
eventProps.referrer = ''
92+
eventProps.path = undefined as any
93+
eventProps.title = null as any
94+
const ctx = await ajs.track('My Event', eventProps)
95+
const page = ctx.event.context!.page
96+
expect(page).toEqual(
97+
expect.objectContaining({ referrer: '', path: undefined, title: null })
98+
)
99+
})
100+
})
101+
46102
test('enriches page events with the page context', async () => {
47103
const ctx = await ajs.page(
48104
'My event',
@@ -51,28 +107,41 @@ describe('Page Enrichment', () => {
51107
)
52108

53109
expect(ctx.event.context?.page).toMatchInlineSnapshot(`
54-
Object {
55-
"path": "/",
56-
"referrer": "",
57-
"search": "",
58-
"title": "",
59-
"url": "not-localhost",
60-
}
61-
`)
110+
Object {
111+
"path": "/",
112+
"referrer": "",
113+
"search": "",
114+
"title": "",
115+
"url": "not-localhost",
116+
}
117+
`)
62118
})
63-
64119
test('enriches page events using properties', async () => {
65120
const ctx = await ajs.page('My event', { banana: 'phone', referrer: 'foo' })
66121

67122
expect(ctx.event.context?.page).toMatchInlineSnapshot(`
68-
Object {
69-
"path": "/",
70-
"referrer": "foo",
71-
"search": "",
72-
"title": "",
73-
"url": "http://localhost/",
74-
}
75-
`)
123+
Object {
124+
"path": "/",
125+
"referrer": "foo",
126+
"search": "",
127+
"title": "",
128+
"url": "http://localhost/",
129+
}
130+
`)
131+
})
132+
133+
test('in page events, event.name overrides event.properties.name', async () => {
134+
const ctx = await ajs.page('My Event', undefined, undefined, {
135+
name: 'some propery name',
136+
})
137+
expect(ctx.event.properties!.name).toBe('My Event')
138+
})
139+
140+
test('in non-page events, event.name does not override event.properties.name', async () => {
141+
const ctx = await ajs.track('My Event', {
142+
name: 'some propery name',
143+
})
144+
expect(ctx.event.properties!.name).toBe('some propery name')
76145
})
77146

78147
test('enriches identify events with the page context', async () => {
@@ -147,4 +216,10 @@ describe('pageDefaults', () => {
147216
const defs = pageDefaults()
148217
expect(defs.url).toEqual('http://www.segment.local?test=true')
149218
})
219+
220+
it('if canonical does not exist, returns fallback', () => {
221+
document.body.appendChild(el)
222+
const defs = pageDefaults()
223+
expect(defs.url).toEqual(window.location.href)
224+
})
150225
})

packages/browser/src/plugins/page-enrichment/index.ts

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { pick } from '../../lib/pick'
12
import type { Context } from '../../core/context'
23
import type { Plugin } from '../../core/plugin'
34

@@ -12,20 +13,12 @@ interface PageDefault {
1213

1314
/**
1415
* Get the current page's canonical URL.
15-
*
16-
* @return {string|undefined}
1716
*/
18-
function canonical(): string {
19-
const tags = document.getElementsByTagName('link')
20-
let canon: string | null = ''
21-
22-
Array.prototype.slice.call(tags).forEach((tag) => {
23-
if (tag.getAttribute('rel') === 'canonical') {
24-
canon = tag.getAttribute('href')
25-
}
26-
})
27-
28-
return canon
17+
function canonical(): string | undefined {
18+
const canon = document.querySelector("link[rel='canonical']")
19+
if (canon) {
20+
return canon.getAttribute('href') || undefined
21+
}
2922
}
3023

3124
/**
@@ -79,24 +72,25 @@ export function pageDefaults(): PageDefault {
7972
function enrichPageContext(ctx: Context): Context {
8073
const event = ctx.event
8174
event.context = event.context || {}
82-
let pageContext = pageDefaults()
83-
const pageProps = event.properties ?? {}
8475

85-
Object.keys(pageContext).forEach((key) => {
86-
if (pageProps[key]) {
87-
pageContext[key] = pageProps[key]
88-
}
89-
})
76+
const defaultPageContext = pageDefaults()
9077

91-
if (event.context.page) {
92-
pageContext = Object.assign({}, pageContext, event.context.page)
93-
}
78+
const pageContextFromEventProps =
79+
event.properties && pick(event.properties, Object.keys(defaultPageContext))
9480

95-
event.context = Object.assign({}, event.context, {
96-
page: pageContext,
97-
})
81+
event.context.page = {
82+
...defaultPageContext,
83+
...pageContextFromEventProps,
84+
...event.context.page,
85+
}
9886

99-
ctx.event = event
87+
if (event.type === 'page') {
88+
event.properties = {
89+
...defaultPageContext,
90+
...event.properties,
91+
...(event.name ? { name: event.name } : {}),
92+
}
93+
}
10094

10195
return ctx
10296
}
@@ -107,21 +101,7 @@ export const pageEnrichment: Plugin = {
107101
isLoaded: () => true,
108102
load: () => Promise.resolve(),
109103
type: 'before',
110-
111-
page: (ctx) => {
112-
ctx.event.properties = Object.assign(
113-
{},
114-
pageDefaults(),
115-
ctx.event.properties
116-
)
117-
118-
if (ctx.event.name) {
119-
ctx.event.properties.name = ctx.event.name
120-
}
121-
122-
return enrichPageContext(ctx)
123-
},
124-
104+
page: enrichPageContext,
125105
alias: enrichPageContext,
126106
track: enrichPageContext,
127107
identify: enrichPageContext,

0 commit comments

Comments
 (0)