Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move context augmentation to Page enrichment #939

Merged
merged 5 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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