@duffcloudservices/telemetry 0.2.0 → 0.3.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 +49 -5
- package/dist/index.d.ts +65 -1
- package/dist/index.js +190 -65
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/composable.test.ts +91 -16
- package/src/composable.ts +393 -80
- package/src/index.ts +7 -2
- package/src/lazySdk.test.ts +698 -0
- package/src/syntheticCapture.test.ts +74 -0
- package/src/syntheticCapture.ts +50 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { SYNTHETIC_CAPTURE_PARAMS, syntheticCaptureDetected } from './syntheticCapture'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* C-614. Both directions matter and they are NOT symmetric:
|
|
7
|
+
*
|
|
8
|
+
* - Under-detecting re-creates the measured defect (49 synthetic vs 14 organic
|
|
9
|
+
* pageViews on coron8 over the 8 days to 2026-08-11).
|
|
10
|
+
* - Over-detecting silently DELETES real visits, and a missing pageView leaves
|
|
11
|
+
* nothing behind to notice it by. The "real visit" cases are therefore the
|
|
12
|
+
* load-bearing half.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const originalLocation = window.location
|
|
16
|
+
|
|
17
|
+
function setSearch(search: string) {
|
|
18
|
+
Object.defineProperty(window, 'location', {
|
|
19
|
+
configurable: true,
|
|
20
|
+
value: { hostname: 'duffcloudservices.com', pathname: '/', search } as Location,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
Object.defineProperty(window, 'location', { configurable: true, value: originalLocation })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('syntheticCaptureDetected', () => {
|
|
29
|
+
it.each(SYNTHETIC_CAPTURE_PARAMS)('detects the valueless %s the capture pass emits', (param) => {
|
|
30
|
+
setSearch(`?${param}=`)
|
|
31
|
+
expect(syntheticCaptureDetected()).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('detects a marker appended after existing parameters', () => {
|
|
35
|
+
setSearch('?utm_source=newsletter&dcs-no-telemetry=')
|
|
36
|
+
expect(syntheticCaptureDetected()).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('counts a bare visit', () => {
|
|
40
|
+
setSearch('')
|
|
41
|
+
expect(syntheticCaptureDetected()).toBe(false)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('counts a campaign visit', () => {
|
|
45
|
+
setSearch('?utm_source=google&utm_medium=cpc&gclid=abc123')
|
|
46
|
+
expect(syntheticCaptureDetected()).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('does not match a lookalike value or a substring', () => {
|
|
50
|
+
setSearch('?ref=dcs-hide-ribbon¬e=dcs-no-telemetry-please')
|
|
51
|
+
expect(syntheticCaptureDetected()).toBe(false)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('re-reads the URL rather than caching the first verdict', () => {
|
|
55
|
+
setSearch('?dcs-no-telemetry=')
|
|
56
|
+
expect(syntheticCaptureDetected()).toBe(true)
|
|
57
|
+
setSearch('')
|
|
58
|
+
expect(syntheticCaptureDetected()).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('fails OPEN when the query string cannot be read', () => {
|
|
62
|
+
Object.defineProperty(window, 'location', {
|
|
63
|
+
configurable: true,
|
|
64
|
+
value: {
|
|
65
|
+
hostname: 'duffcloudservices.com',
|
|
66
|
+
pathname: '/',
|
|
67
|
+
get search(): string {
|
|
68
|
+
throw new Error('unreadable location')
|
|
69
|
+
},
|
|
70
|
+
} as unknown as Location,
|
|
71
|
+
})
|
|
72
|
+
expect(syntheticCaptureDetected()).toBe(false)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthetic-capture detection (C-614).
|
|
3
|
+
*
|
|
4
|
+
* The post-deploy "Capture page snapshots" CI step drives real Chromium against
|
|
5
|
+
* the LIVE production URL of the site it just deployed, so the site's telemetry
|
|
6
|
+
* boots exactly as it would for a visitor and every page the capture walked is
|
|
7
|
+
* recorded as a production pageView — during our own deploy run. Measured on
|
|
8
|
+
* coron8 over the 8 days to 2026-08-11: 49 synthetic pageViews against 14
|
|
9
|
+
* organic, 78% of the entire store, all 49 on 08-11 across 5 deploys versus 3
|
|
10
|
+
* real visits that day.
|
|
11
|
+
*
|
|
12
|
+
* The capture tooling marks every URL it loads (`packages/cli/src/commands/
|
|
13
|
+
* capture-snapshots.ts` and each site's own `scripts/capture-snapshots.*`).
|
|
14
|
+
* `initialize()` reads that mark and declines, so nothing is emitted at all.
|
|
15
|
+
* Suppressing here rather than tagging and filtering downstream keeps the
|
|
16
|
+
* denominator honest in every read path at once instead of obliging each of
|
|
17
|
+
* them to remember an exclusion — the C-430 preview/localhost blend is the
|
|
18
|
+
* recorded UNSOLVED residual that shape produces.
|
|
19
|
+
*
|
|
20
|
+
* DELIBERATE DUPLICATION. `@duffcloudservices/cms` carries the same predicate in
|
|
21
|
+
* its own `syntheticCapture.ts`. These two packages are intentionally decoupled
|
|
22
|
+
* — this one is the low-level telemetry package (peer deps: the App Insights SDK
|
|
23
|
+
* and vue, nothing else), cms is the site-content package — and a dependency
|
|
24
|
+
* edge between them to share ten lines of pure string matching would cost more
|
|
25
|
+
* than the duplication does. The parameter list is the contract; keep the two
|
|
26
|
+
* lists identical, and note that the CI drift guard in the canonical
|
|
27
|
+
* site-deploy workflow greps for these same names.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Query parameters that mark a document as an automated capture, not a visit. */
|
|
31
|
+
export const SYNTHETIC_CAPTURE_PARAMS = ['dcs-hide-ribbon', 'dcs-no-telemetry'] as const
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether this document is an automated capture rather than a visit worth counting.
|
|
35
|
+
*
|
|
36
|
+
* Reads `window.location.search` on every call rather than caching, so a
|
|
37
|
+
* client-side route change is judged on the CURRENT URL. Returns `false` on the
|
|
38
|
+
* server, and `false` when the query string cannot be read: over-suppression
|
|
39
|
+
* deletes real visits and leaves no trace to notice it by, so this fails OPEN.
|
|
40
|
+
*/
|
|
41
|
+
export function syntheticCaptureDetected(): boolean {
|
|
42
|
+
if (typeof window === 'undefined') return false
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const params = new URLSearchParams(window.location.search)
|
|
46
|
+
return SYNTHETIC_CAPTURE_PARAMS.some((param) => params.has(param))
|
|
47
|
+
} catch {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
}
|