Skip to content

Commit

Permalink
Move context augmentation to Page enrichment (#939)
Browse files Browse the repository at this point in the history
Part of segment.atlassian.net/browse/LIBWEB-1426
  • Loading branch information
zikaari authored Aug 31, 2023
1 parent 6a028e0 commit ee855ba
Show file tree
Hide file tree
Showing 11 changed files with 531 additions and 475 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-camels-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@segment/analytics-next': minor
---

Move context augmentation to Page Enrichment plugin
5 changes: 5 additions & 0 deletions .changeset/calm-donkeys-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@segment/analytics-core': patch
---

Update Campaign type to be more relaxed
2 changes: 1 addition & 1 deletion packages/browser/src/browser/browser-umd.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getCDN, setGlobalCDNUrl } from '../lib/parse-cdn'
import { setVersionType } from '../plugins/segmentio/normalize'
import { setVersionType } from '../lib/version-type'

if (process.env.ASSET_PATH) {
if (process.env.ASSET_PATH === '/dist/umd/') {
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/browser/standalone.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { getCDN, setGlobalCDNUrl } from '../lib/parse-cdn'
import { setVersionType } from '../plugins/segmentio/normalize'
import { setVersionType } from '../lib/version-type'

if (process.env.ASSET_PATH) {
if (process.env.ASSET_PATH === '/dist/umd/') {
Expand Down
2 changes: 1 addition & 1 deletion packages/browser/src/core/stats/remote-metrics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fetch } from '../../lib/fetch'
import { version } from '../../generated/version'
import { getVersionType } from '../../plugins/segmentio/normalize'
import { getVersionType } from '../../lib/version-type'
import { SEGMENT_API_HOST } from '../constants'

export interface MetricsOptions {
Expand Down
10 changes: 10 additions & 0 deletions packages/browser/src/lib/version-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Default value will be updated to 'web' in `bundle-umd.ts` for web build.
let _version: 'web' | 'npm' = 'npm'

export function setVersionType(version: typeof _version) {
_version = version
}

export function getVersionType(): typeof _version {
return _version
}
326 changes: 326 additions & 0 deletions packages/browser/src/plugins/page-enrichment/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import cookie from 'js-cookie'
import assert from 'assert'
import { Analytics } from '../../../core/analytics'
import { pageEnrichment, pageDefaults } from '..'
import { pick } from '../../../lib/pick'
import { SegmentioSettings } from '../../segmentio'
import { version } from '../../../generated/version'
import { CoreExtraContext } from '@segment/analytics-core'

let ajs: Analytics

Expand All @@ -16,6 +21,20 @@ const helpers = {
},
}

/**
* Filters out the calls made for probing cookie availability
*/
const ignoreProbeCookieWrites = (
fn: jest.SpyInstance<
string | undefined,
[
name: string,
value: string | object,
options?: cookie.CookieAttributes | undefined
]
>
) => fn.mock.calls.filter((c) => c[0] !== 'ajs_cookies_check')

describe('Page Enrichment', () => {
beforeEach(async () => {
ajs = new Analytics({
Expand Down Expand Up @@ -239,3 +258,310 @@ describe('pageDefaults', () => {
expect(defs.url).toEqual(window.location.href)
})
})

describe('Other visitor metadata', () => {
let options: SegmentioSettings
let analytics: Analytics

const amendSearchParams = (search?: any): CoreExtraContext => ({
page: { search },
})

beforeEach(async () => {
options = { apiKey: 'foo' }
analytics = new Analytics({ writeKey: options.apiKey })

await analytics.register(pageEnrichment)
})

afterEach(() => {
analytics.reset()
Object.keys(cookie.get()).map((k) => cookie.remove(k))

if (window.localStorage) {
window.localStorage.clear()
}
})

it('should add .library', async () => {
const ctx = await analytics.track('test')
assert(ctx.event.context?.library)
assert(ctx.event.context?.library.name === 'analytics.js')
assert(ctx.event.context?.library.version === `npm:next-${version}`)
})

it('should allow override of .library', async () => {
const customContext = {
library: {
name: 'analytics-wordpress',
version: '1.0.3',
},
}

const ctx = await analytics.track('test', {}, { context: customContext })

assert(ctx.event.context?.library)
assert(ctx.event.context?.library.name === 'analytics-wordpress')
assert(ctx.event.context?.library.version === '1.0.3')
})

it('should add .userAgent', async () => {
const ctx = await analytics.track('test')
const removeVersionNum = (agent: string) => agent.replace(/jsdom\/.*/, '')
const userAgent1 = removeVersionNum(ctx.event.context?.userAgent as string)
const userAgent2 = removeVersionNum(navigator.userAgent)
assert(userAgent1 === userAgent2)
})

it('should add .locale', async () => {
const ctx = await analytics.track('test')
assert(ctx.event.context?.locale === navigator.language)
})

it('should not replace .locale if provided', async () => {
const customContext = {
locale: 'foobar',
}

const ctx = await analytics.track('test', {}, { context: customContext })
assert(ctx.event.context?.locale === 'foobar')
})

it('should add .campaign', async () => {
const ctx = await analytics.track(
'test',
{},
{
context: amendSearchParams(
'utm_source=source&utm_medium=medium&utm_term=term&utm_content=content&utm_campaign=name'
),
}
)

assert(ctx.event)
assert(ctx.event.context)
assert(ctx.event.context.campaign)
assert(ctx.event.context.campaign.source === 'source')
assert(ctx.event.context.campaign.medium === 'medium')
assert(ctx.event.context.campaign.term === 'term')
assert(ctx.event.context.campaign.content === 'content')
assert(ctx.event.context.campaign.name === 'name')
})

it('should decode query params', async () => {
const ctx = await analytics.track(
'test',
{},
{
context: amendSearchParams('?utm_source=%5BFoo%5D'),
}
)

assert(ctx.event)
assert(ctx.event.context)
assert(ctx.event.context.campaign)
assert(ctx.event.context.campaign.source === '[Foo]')
})

it('should guard against undefined utm params', async () => {
const ctx = await analytics.track(
'test',
{},
{
context: amendSearchParams('?utm_source'),
}
)

assert(ctx.event)
assert(ctx.event.context)
assert(ctx.event.context.campaign)
assert(ctx.event.context.campaign.source === '')
})

it('should guard against empty utm params', async () => {
const ctx = await analytics.track(
'test',
{},
{
context: amendSearchParams('?utm_source='),
}
)

assert(ctx.event)
assert(ctx.event.context)
assert(ctx.event.context.campaign)
assert(ctx.event.context.campaign.source === '')
})

it('only parses utm params suffixed with _', async () => {
const ctx = await analytics.track(
'test',
{},
{
context: amendSearchParams('?utm'),
}
)

assert(ctx.event)
assert(ctx.event.context)
assert.deepStrictEqual(ctx.event.context.campaign, {})
})

it('should guard against short utm params', async () => {
const ctx = await analytics.track(
'test',
{},
{
context: amendSearchParams('?utm_'),
}
)

assert(ctx.event)
assert(ctx.event.context)
assert.deepStrictEqual(ctx.event.context.campaign, {})
})

it('should allow override of .campaign', async () => {
const ctx = await analytics.track(
'test',
{},
{
context: {
...amendSearchParams(
'?utm_source=source&utm_medium=medium&utm_term=term&utm_content=content&utm_campaign=name'
),
campaign: {
source: 'overrideSource',
medium: 'overrideMedium',
term: 'overrideTerm',
content: 'overrideContent',
name: 'overrideName',
},
},
}
)

assert(ctx.event)
assert(ctx.event.context)
assert(ctx.event.context.campaign)
assert(ctx.event.context.campaign.source === 'overrideSource')
assert(ctx.event.context.campaign.medium === 'overrideMedium')
assert(ctx.event.context.campaign.term === 'overrideTerm')
assert(ctx.event.context.campaign.content === 'overrideContent')
assert(ctx.event.context.campaign.name === 'overrideName')
})

it('should allow override of .search with object', async () => {
const ctx = await analytics.track(
'test',
{},
{
context: amendSearchParams({
someObject: 'foo',
}),
}
)
assert(ctx.event)
assert(ctx.event.context)
assert(ctx.event.context.campaign === undefined)
assert(ctx.event.context.referrer === undefined)
})

it('should add .referrer.id and .referrer.type (cookies)', async () => {
const ctx = await analytics.track(
'test',
{},
{
context: amendSearchParams('?utm_source=source&urid=medium'),
}
)

assert(ctx.event)
assert(ctx.event.context)
assert(ctx.event.context.referrer)
expect(ctx.event.context.referrer.id).toBe('medium')
assert(ctx.event.context.referrer.type === 'millennial-media')
expect(cookie.get('s:context.referrer')).toEqual(
JSON.stringify({
id: 'medium',
type: 'millennial-media',
})
)
})

it('should add .referrer.id and .referrer.type (cookieless)', async () => {
const setCookieSpy = jest.spyOn(cookie, 'set')
analytics = new Analytics(
{ writeKey: options.apiKey },
{ disableClientPersistence: true }
)

await analytics.register(pageEnrichment)

const ctx = await analytics.track(
'test',
{},
{
context: amendSearchParams('utm_source=source&urid=medium'),
}
)

assert(ctx.event)
assert(ctx.event.context)
assert(ctx.event.context.referrer)
expect(ctx.event.context.referrer.id).toEqual('medium')
assert(ctx.event.context.referrer.type === 'millennial-media')
expect(cookie.get('s:context.referrer')).toBeUndefined()
expect(ignoreProbeCookieWrites(setCookieSpy).length).toBe(0)
})

it('should add .referrer.id and .referrer.type from cookie', async () => {
cookie.set('s:context.referrer', '{"id":"baz","type":"millennial-media"}')
const ctx = await analytics.track('test')

assert(ctx.event)
assert(ctx.event.context)
assert(ctx.event.context.referrer)
assert(ctx.event.context.referrer.id === 'baz')
assert(ctx.event.context.referrer.type === 'millennial-media')
})

it('should add .referrer.id and .referrer.type from cookie when no query is given', async () => {
cookie.set(
's:context.referrer',
'{"id":"medium","type":"millennial-media"}'
)
const ctx = await analytics.track('test')

assert(ctx.event)
assert(ctx.event.context)
assert(ctx.event.context.referrer)
assert(ctx.event.context.referrer.id === 'medium')
assert(ctx.event.context.referrer.type === 'millennial-media')
})

it('shouldnt add non amp ga cookie', async () => {
cookie.set('_ga', 'some-nonamp-id')
const ctx = await analytics.track('test')
assert(ctx.event)
assert(ctx.event.context)
assert(!ctx.event.context.amp)
})

it('should add .amp.id from store', async () => {
cookie.set('_ga', 'amp-foo')
const ctx = await analytics.track('test')
assert(ctx.event)
assert(ctx.event.context)
assert(ctx.event.context.amp)
assert(ctx.event.context.amp.id === 'amp-foo')
})

it('should not add .amp if theres no _ga', async () => {
cookie.remove('_ga')
const ctx = await analytics.track('test')
assert(ctx.event)
assert(ctx.event.context)
assert(!ctx.event.context.amp)
})
})
Loading

0 comments on commit ee855ba

Please sign in to comment.