@duffcloudservices/telemetry 0.1.0 → 0.2.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/README.md +37 -1
- package/dist/chunk-DKX4AC2U.js +131 -0
- package/dist/chunk-DKX4AC2U.js.map +1 -0
- package/dist/{config-BPmCpvmt.d.ts → config-F0cscN3Z.d.ts} +11 -0
- package/dist/config.d.ts +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +64 -9
- package/dist/index.js.map +1 -1
- package/dist/journey.d.ts +111 -0
- package/dist/journey.js +3 -0
- package/dist/journey.js.map +1 -0
- package/package.json +39 -11
- package/src/composable.test.ts +199 -3
- package/src/composable.ts +178 -9
- package/src/index.ts +16 -0
- package/src/journey.test.ts +135 -0
- package/src/journey.ts +280 -0
- package/src/types.ts +11 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
captureJourneyFirstTouch,
|
|
5
|
+
getJourneyContext,
|
|
6
|
+
JOURNEY_FIELD_MAX_LENGTH,
|
|
7
|
+
JOURNEY_FIRST_TOUCH_KEY,
|
|
8
|
+
setJourneyIdentityResolver,
|
|
9
|
+
toJourneyTelemetryPayload,
|
|
10
|
+
} from './journey'
|
|
11
|
+
|
|
12
|
+
function clearCookies(): void {
|
|
13
|
+
for (const part of document.cookie.split(';')) {
|
|
14
|
+
const name = part.split('=')[0]?.trim()
|
|
15
|
+
if (name) document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function navigateTo(url: string): void {
|
|
20
|
+
window.history.replaceState({}, '', url)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('journey', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
sessionStorage.clear()
|
|
26
|
+
clearCookies()
|
|
27
|
+
setJourneyIdentityResolver(null)
|
|
28
|
+
navigateTo('/')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
setJourneyIdentityResolver(null)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('captures referrer, landing path and utm once per session', () => {
|
|
36
|
+
Object.defineProperty(document, 'referrer', {
|
|
37
|
+
value: 'https://www.google.com/',
|
|
38
|
+
configurable: true,
|
|
39
|
+
})
|
|
40
|
+
navigateTo('/compare/booksy-alternative?utm_source=google&utm_medium=organic')
|
|
41
|
+
|
|
42
|
+
captureJourneyFirstTouch()
|
|
43
|
+
|
|
44
|
+
const snapshot = JSON.parse(sessionStorage.getItem(JOURNEY_FIRST_TOUCH_KEY) ?? '{}')
|
|
45
|
+
expect(snapshot.referrer).toBe('https://www.google.com/')
|
|
46
|
+
expect(snapshot.landingPath).toBe(
|
|
47
|
+
'/compare/booksy-alternative?utm_source=google&utm_medium=organic',
|
|
48
|
+
)
|
|
49
|
+
expect(snapshot.utm).toEqual({ source: 'google', medium: 'organic' })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('never overwrites the first-touch snapshot on a later page', () => {
|
|
53
|
+
Object.defineProperty(document, 'referrer', {
|
|
54
|
+
value: 'https://www.google.com/',
|
|
55
|
+
configurable: true,
|
|
56
|
+
})
|
|
57
|
+
navigateTo('/compare/booksy-alternative')
|
|
58
|
+
captureJourneyFirstTouch()
|
|
59
|
+
|
|
60
|
+
// The SPA navigates; document.referrer is gone by the time the form renders.
|
|
61
|
+
Object.defineProperty(document, 'referrer', { value: '', configurable: true })
|
|
62
|
+
navigateTo('/contact?plan=Professional')
|
|
63
|
+
captureJourneyFirstTouch()
|
|
64
|
+
|
|
65
|
+
const context = getJourneyContext()
|
|
66
|
+
expect(context.referrer).toBe('https://www.google.com/')
|
|
67
|
+
expect(context.landingPath).toBe('/compare/booksy-alternative')
|
|
68
|
+
// ...while the submit-time page is the contact page, not the landing page.
|
|
69
|
+
expect(context.pagePath).toBe('/contact')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('falls back to the ai_user / ai_session cookies for identity', () => {
|
|
73
|
+
document.cookie = 'ai_user=oA7p7yUdBDbu0bXCR6WACW|2026-07-30T12:54:01.000Z; path=/'
|
|
74
|
+
document.cookie = 'ai_session=Yb3kZ|2026-07-30T12:54:01.000Z|2026-07-30T12:55:47.000Z; path=/'
|
|
75
|
+
|
|
76
|
+
const context = getJourneyContext()
|
|
77
|
+
expect(context.visitorId).toBe('oA7p7yUdBDbu0bXCR6WACW')
|
|
78
|
+
expect(context.sessionId).toBe('Yb3kZ')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('prefers the live SDK identity over the cookies', () => {
|
|
82
|
+
document.cookie = 'ai_user=cookie-user|2026-07-30T12:54:01.000Z; path=/'
|
|
83
|
+
setJourneyIdentityResolver(() => ({ visitorId: 'sdk-user', sessionId: 'sdk-session' }))
|
|
84
|
+
|
|
85
|
+
const context = getJourneyContext()
|
|
86
|
+
expect(context.visitorId).toBe('sdk-user')
|
|
87
|
+
expect(context.sessionId).toBe('sdk-session')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('survives a throwing identity resolver', () => {
|
|
91
|
+
document.cookie = 'ai_user=cookie-user|2026-07-30T12:54:01.000Z; path=/'
|
|
92
|
+
setJourneyIdentityResolver(() => {
|
|
93
|
+
throw new Error('SDK context unavailable')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
expect(() => getJourneyContext()).not.toThrow()
|
|
97
|
+
expect(getJourneyContext().visitorId).toBe('cookie-user')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('clamps every field to the server-side maximum', () => {
|
|
101
|
+
const long = 'x'.repeat(JOURNEY_FIELD_MAX_LENGTH + 50)
|
|
102
|
+
document.cookie = `ai_user=${long}|2026-07-30T12:54:01.000Z; path=/`
|
|
103
|
+
|
|
104
|
+
const context = getJourneyContext()
|
|
105
|
+
expect(context.visitorId).toHaveLength(JOURNEY_FIELD_MAX_LENGTH)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('omits the payload entirely when there is nothing to report', () => {
|
|
109
|
+
Object.defineProperty(document, 'referrer', { value: '', configurable: true })
|
|
110
|
+
navigateTo('/')
|
|
111
|
+
sessionStorage.clear()
|
|
112
|
+
|
|
113
|
+
// A bare "/" landing with no referrer, no utm and no cookies still reports
|
|
114
|
+
// the submit-time page, which is legitimately useful.
|
|
115
|
+
const payload = toJourneyTelemetryPayload()
|
|
116
|
+
expect(payload?.pagePath).toBe('/')
|
|
117
|
+
expect(payload?.visitorId).toBeUndefined()
|
|
118
|
+
|
|
119
|
+
// ...but an empty context flattens to undefined so the caller omits the field.
|
|
120
|
+
expect(toJourneyTelemetryPayload({})).toBeUndefined()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('flattens utm into the contract shape', () => {
|
|
124
|
+
const payload = toJourneyTelemetryPayload({
|
|
125
|
+
visitorId: 'v1',
|
|
126
|
+
utm: { source: 'google', medium: 'cpc', campaign: 'summer' },
|
|
127
|
+
})
|
|
128
|
+
expect(payload).toEqual({
|
|
129
|
+
visitorId: 'v1',
|
|
130
|
+
utmSource: 'google',
|
|
131
|
+
utmMedium: 'cpc',
|
|
132
|
+
utmCampaign: 'summer',
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
})
|
package/src/journey.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visitor-journey context for form submissions.
|
|
3
|
+
*
|
|
4
|
+
* A submitted form row and the site's Application Insights telemetry describe
|
|
5
|
+
* the same visit, but nothing joins them: the row knows the message, the
|
|
6
|
+
* telemetry knows the path that produced it. `getJourneyContext()` returns the
|
|
7
|
+
* join key — the ids the SDK is already stamping on every event, plus the
|
|
8
|
+
* first-touch attribution the SPA throws away.
|
|
9
|
+
*
|
|
10
|
+
* WHY FIRST-TOUCH IS CAPTURED AT SESSION START, NOT AT SUBMIT. `document.referrer`
|
|
11
|
+
* is only populated for the document the browser actually loaded. The moment a
|
|
12
|
+
* client-side router takes over, it is gone — so a visitor who lands from Google
|
|
13
|
+
* on a comparison page and clicks through to /contact submits with an empty
|
|
14
|
+
* referrer. Capture-at-submit is not "less accurate" here; it is structurally
|
|
15
|
+
* always empty for the multi-page journeys worth attributing. So
|
|
16
|
+
* `captureJourneyFirstTouch()` snapshots the referrer, landing path, and utm_*
|
|
17
|
+
* parameters once per session into `sessionStorage`, and every later read serves
|
|
18
|
+
* that snapshot.
|
|
19
|
+
*
|
|
20
|
+
* This module deliberately imports nothing. It must stay usable by a consumer
|
|
21
|
+
* that has no App Insights SDK loaded (the portal runs its own hand-rolled
|
|
22
|
+
* telemetry boot), and it must never be the reason a form submission fails —
|
|
23
|
+
* every entry point swallows its own errors and degrades to an empty result.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** First-touch UTM parameters parsed from the landing URL. */
|
|
27
|
+
export interface JourneyUtm {
|
|
28
|
+
source?: string
|
|
29
|
+
medium?: string
|
|
30
|
+
campaign?: string
|
|
31
|
+
term?: string
|
|
32
|
+
content?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The join key attached to an outbound form submission. */
|
|
36
|
+
export interface JourneyContext {
|
|
37
|
+
/** App Insights user id — the visitor, across sessions. */
|
|
38
|
+
visitorId?: string
|
|
39
|
+
/** App Insights session id — this visit. */
|
|
40
|
+
sessionId?: string
|
|
41
|
+
/** Route path the form was submitted from. */
|
|
42
|
+
pagePath?: string
|
|
43
|
+
/** Full URL the form was submitted from. */
|
|
44
|
+
pageUrl?: string
|
|
45
|
+
/** First-touch `document.referrer` for this session. */
|
|
46
|
+
referrer?: string
|
|
47
|
+
/** First-touch landing path (pathname + query) for this session. */
|
|
48
|
+
landingPath?: string
|
|
49
|
+
/** First-touch UTM parameters, when the landing URL carried any. */
|
|
50
|
+
utm?: JourneyUtm
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Resolves visitor/session ids from a live telemetry SDK instance. */
|
|
54
|
+
export type JourneyIdentityResolver = () =>
|
|
55
|
+
| { visitorId?: string; sessionId?: string }
|
|
56
|
+
| undefined
|
|
57
|
+
|
|
58
|
+
/** sessionStorage key holding this session's first-touch snapshot. */
|
|
59
|
+
export const JOURNEY_FIRST_TOUCH_KEY = 'dcs:journey:first-touch'
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Per-field clamp. Mirrors the server's `journey.FieldMaxLength` so a value that
|
|
63
|
+
* survives here is never silently truncated on the way into storage.
|
|
64
|
+
*/
|
|
65
|
+
export const JOURNEY_FIELD_MAX_LENGTH = 512
|
|
66
|
+
|
|
67
|
+
interface FirstTouchSnapshot {
|
|
68
|
+
referrer?: string
|
|
69
|
+
landingPath?: string
|
|
70
|
+
utm?: JourneyUtm
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let identityResolver: JourneyIdentityResolver | null = null
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Register the live-SDK source for visitor/session ids.
|
|
77
|
+
*
|
|
78
|
+
* The composable calls this after `loadAppInsights()`. Without it — or when the
|
|
79
|
+
* resolver returns nothing — {@link getJourneyContext} falls back to parsing the
|
|
80
|
+
* `ai_user` / `ai_session` cookies the SDK writes, which is why the portal (with
|
|
81
|
+
* its own SDK boot and no call to this function) still reports usable ids.
|
|
82
|
+
*/
|
|
83
|
+
export function setJourneyIdentityResolver(
|
|
84
|
+
resolver: JourneyIdentityResolver | null,
|
|
85
|
+
): void {
|
|
86
|
+
identityResolver = resolver
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function clamp(value: string | undefined | null): string | undefined {
|
|
90
|
+
if (typeof value !== 'string') return undefined
|
|
91
|
+
const trimmed = value.trim()
|
|
92
|
+
if (!trimmed) return undefined
|
|
93
|
+
return trimmed.length > JOURNEY_FIELD_MAX_LENGTH
|
|
94
|
+
? trimmed.slice(0, JOURNEY_FIELD_MAX_LENGTH)
|
|
95
|
+
: trimmed
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read a cookie value, URL-decoded. Returns undefined when absent.
|
|
100
|
+
*/
|
|
101
|
+
function readCookie(name: string): string | undefined {
|
|
102
|
+
if (typeof document === 'undefined' || typeof document.cookie !== 'string') {
|
|
103
|
+
return undefined
|
|
104
|
+
}
|
|
105
|
+
const prefix = `${name}=`
|
|
106
|
+
for (const part of document.cookie.split(';')) {
|
|
107
|
+
const entry = part.trim()
|
|
108
|
+
if (!entry.startsWith(prefix)) continue
|
|
109
|
+
const raw = entry.slice(prefix.length)
|
|
110
|
+
try {
|
|
111
|
+
return decodeURIComponent(raw)
|
|
112
|
+
} catch {
|
|
113
|
+
return raw
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The App Insights cookies are pipe-delimited composites:
|
|
121
|
+
* `ai_user=<userId>|<acquisitionDate>` and
|
|
122
|
+
* `ai_session=<sessionId>|<acquisitionDate>|<renewalDate>`. Only the leading
|
|
123
|
+
* segment is the id.
|
|
124
|
+
*/
|
|
125
|
+
function firstCookieSegment(name: string): string | undefined {
|
|
126
|
+
const value = readCookie(name)
|
|
127
|
+
if (!value) return undefined
|
|
128
|
+
return clamp(value.split('|')[0])
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseUtm(params: URLSearchParams): JourneyUtm | undefined {
|
|
132
|
+
const utm: JourneyUtm = {}
|
|
133
|
+
const source = clamp(params.get('utm_source'))
|
|
134
|
+
const medium = clamp(params.get('utm_medium'))
|
|
135
|
+
const campaign = clamp(params.get('utm_campaign'))
|
|
136
|
+
const term = clamp(params.get('utm_term'))
|
|
137
|
+
const content = clamp(params.get('utm_content'))
|
|
138
|
+
if (source) utm.source = source
|
|
139
|
+
if (medium) utm.medium = medium
|
|
140
|
+
if (campaign) utm.campaign = campaign
|
|
141
|
+
if (term) utm.term = term
|
|
142
|
+
if (content) utm.content = content
|
|
143
|
+
return Object.keys(utm).length > 0 ? utm : undefined
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readFirstTouch(): FirstTouchSnapshot | undefined {
|
|
147
|
+
if (typeof sessionStorage === 'undefined') return undefined
|
|
148
|
+
try {
|
|
149
|
+
const raw = sessionStorage.getItem(JOURNEY_FIRST_TOUCH_KEY)
|
|
150
|
+
if (!raw) return undefined
|
|
151
|
+
const parsed = JSON.parse(raw) as FirstTouchSnapshot
|
|
152
|
+
return parsed && typeof parsed === 'object' ? parsed : undefined
|
|
153
|
+
} catch {
|
|
154
|
+
return undefined
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Snapshot this session's first-touch context, once.
|
|
160
|
+
*
|
|
161
|
+
* Idempotent and safe to call from anywhere: a snapshot already in
|
|
162
|
+
* sessionStorage always wins, so a later call on a deep page can never overwrite
|
|
163
|
+
* the real landing page with the current one. Called by the composable's
|
|
164
|
+
* `initialize()` (as early in the app's life as telemetry exists) and lazily by
|
|
165
|
+
* {@link getJourneyContext} so a consumer that never initializes the shared SDK
|
|
166
|
+
* still records *something* rather than nothing.
|
|
167
|
+
*/
|
|
168
|
+
export function captureJourneyFirstTouch(): void {
|
|
169
|
+
try {
|
|
170
|
+
if (typeof window === 'undefined' || typeof sessionStorage === 'undefined') {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
if (sessionStorage.getItem(JOURNEY_FIRST_TOUCH_KEY)) return
|
|
174
|
+
|
|
175
|
+
const snapshot: FirstTouchSnapshot = {}
|
|
176
|
+
const referrer =
|
|
177
|
+
typeof document !== 'undefined' ? clamp(document.referrer) : undefined
|
|
178
|
+
if (referrer) snapshot.referrer = referrer
|
|
179
|
+
|
|
180
|
+
const landingPath = clamp(
|
|
181
|
+
`${window.location.pathname}${window.location.search}`,
|
|
182
|
+
)
|
|
183
|
+
if (landingPath) snapshot.landingPath = landingPath
|
|
184
|
+
|
|
185
|
+
const utm = parseUtm(new URLSearchParams(window.location.search))
|
|
186
|
+
if (utm) snapshot.utm = utm
|
|
187
|
+
|
|
188
|
+
sessionStorage.setItem(JOURNEY_FIRST_TOUCH_KEY, JSON.stringify(snapshot))
|
|
189
|
+
} catch {
|
|
190
|
+
// sessionStorage can throw under blocked-storage policies. Journey capture
|
|
191
|
+
// is an enrichment, never a requirement — stay silent and degrade.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Build the journey context to attach to an outbound form submission.
|
|
197
|
+
*
|
|
198
|
+
* NEVER THROWS. Returns `{}` when there is nothing to report (SSR, blocked
|
|
199
|
+
* storage, telemetry-dark site), and callers omit the field entirely in that
|
|
200
|
+
* case — a telemetry failure must not cost a lead.
|
|
201
|
+
*/
|
|
202
|
+
export function getJourneyContext(): JourneyContext {
|
|
203
|
+
try {
|
|
204
|
+
if (typeof window === 'undefined') return {}
|
|
205
|
+
|
|
206
|
+
captureJourneyFirstTouch()
|
|
207
|
+
|
|
208
|
+
const context: JourneyContext = {}
|
|
209
|
+
|
|
210
|
+
const identity = (() => {
|
|
211
|
+
try {
|
|
212
|
+
return identityResolver?.()
|
|
213
|
+
} catch {
|
|
214
|
+
return undefined
|
|
215
|
+
}
|
|
216
|
+
})()
|
|
217
|
+
|
|
218
|
+
const visitorId = clamp(identity?.visitorId) ?? firstCookieSegment('ai_user')
|
|
219
|
+
const sessionId =
|
|
220
|
+
clamp(identity?.sessionId) ?? firstCookieSegment('ai_session')
|
|
221
|
+
if (visitorId) context.visitorId = visitorId
|
|
222
|
+
if (sessionId) context.sessionId = sessionId
|
|
223
|
+
|
|
224
|
+
const pagePath = clamp(window.location.pathname)
|
|
225
|
+
const pageUrl = clamp(window.location.href)
|
|
226
|
+
if (pagePath) context.pagePath = pagePath
|
|
227
|
+
if (pageUrl) context.pageUrl = pageUrl
|
|
228
|
+
|
|
229
|
+
const firstTouch = readFirstTouch()
|
|
230
|
+
const referrer = clamp(firstTouch?.referrer)
|
|
231
|
+
const landingPath = clamp(firstTouch?.landingPath)
|
|
232
|
+
if (referrer) context.referrer = referrer
|
|
233
|
+
if (landingPath) context.landingPath = landingPath
|
|
234
|
+
if (firstTouch?.utm && Object.keys(firstTouch.utm).length > 0) {
|
|
235
|
+
context.utm = firstTouch.utm
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return context
|
|
239
|
+
} catch {
|
|
240
|
+
return {}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The wire shape the DCS submission contracts accept (`SubmissionTelemetry`). */
|
|
245
|
+
export interface JourneyTelemetryPayload {
|
|
246
|
+
visitorId?: string
|
|
247
|
+
sessionId?: string
|
|
248
|
+
pagePath?: string
|
|
249
|
+
pageUrl?: string
|
|
250
|
+
referrer?: string
|
|
251
|
+
landingPath?: string
|
|
252
|
+
utmSource?: string
|
|
253
|
+
utmMedium?: string
|
|
254
|
+
utmCampaign?: string
|
|
255
|
+
utmTerm?: string
|
|
256
|
+
utmContent?: string
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Flatten a {@link JourneyContext} into the contract's `telemetry` object, or
|
|
261
|
+
* return `undefined` when there is nothing worth sending so the caller can omit
|
|
262
|
+
* the field rather than post an empty object.
|
|
263
|
+
*/
|
|
264
|
+
export function toJourneyTelemetryPayload(
|
|
265
|
+
context: JourneyContext = getJourneyContext(),
|
|
266
|
+
): JourneyTelemetryPayload | undefined {
|
|
267
|
+
const payload: JourneyTelemetryPayload = {}
|
|
268
|
+
if (context.visitorId) payload.visitorId = context.visitorId
|
|
269
|
+
if (context.sessionId) payload.sessionId = context.sessionId
|
|
270
|
+
if (context.pagePath) payload.pagePath = context.pagePath
|
|
271
|
+
if (context.pageUrl) payload.pageUrl = context.pageUrl
|
|
272
|
+
if (context.referrer) payload.referrer = context.referrer
|
|
273
|
+
if (context.landingPath) payload.landingPath = context.landingPath
|
|
274
|
+
if (context.utm?.source) payload.utmSource = context.utm.source
|
|
275
|
+
if (context.utm?.medium) payload.utmMedium = context.utm.medium
|
|
276
|
+
if (context.utm?.campaign) payload.utmCampaign = context.utm.campaign
|
|
277
|
+
if (context.utm?.term) payload.utmTerm = context.utm.term
|
|
278
|
+
if (context.utm?.content) payload.utmContent = context.utm.content
|
|
279
|
+
return Object.keys(payload).length > 0 ? payload : undefined
|
|
280
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -18,6 +18,17 @@ export interface TelemetryPageView {
|
|
|
18
18
|
uri?: string
|
|
19
19
|
properties?: Record<string, string>
|
|
20
20
|
measurements?: Record<string, number>
|
|
21
|
+
/**
|
|
22
|
+
* Send this page view even though the SDK's `enableAutoRouteTracking` is active.
|
|
23
|
+
*
|
|
24
|
+
* Off by default, and it should stay off: with auto-route tracking on, the SDK
|
|
25
|
+
* already emits one pageView per navigation, so a manual one double-counts the
|
|
26
|
+
* page. C-288 measured that exact configuration producing 52% duplicate page views
|
|
27
|
+
* on a live customer site, which the portal then reported to the owner. Set this
|
|
28
|
+
* only when you have verified auto-route tracking does not fire for your routing
|
|
29
|
+
* setup; prefer `useTelemetry({ enableAutoRouteTracking: false })` instead.
|
|
30
|
+
*/
|
|
31
|
+
force?: boolean
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
export interface TelemetryException {
|