Skip to content
Open
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
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ Repository-wide baseline. Child files add local constraints; the nearest child f
- Validate package and implementation changes in dependency order: source package typecheck/tests,
source package build, `pnpm build:pkgs` when implementations consume it, implementation install,
then downstream checks.
- Treat package build outputs as shared mutable state across all SDKs. Never manually run `build`,
`clean`, `build:pkgs`, implementation install, `size:report`, `size:check`, or any command that
reads, writes, removes, or packages generated artifacts in parallel with another package command
that can touch the same package or an upstream/downstream package in its dependency graph.
- `size:report` and `size:check` read generated package output and may depend on emitted chunks from
upstream packages. Serialize them with any build, clean, package, or size command for the package
being measured and every upstream or downstream SDK that can consume its output.
- Do not manually parallelize validation for packages with dependency edges between them. Prefer the
aggregate workspace command, such as `pnpm build`, `pnpm build:pkgs`, or `pnpm size:check`, when
the full graph is involved because pnpm can schedule workspace dependencies. When running narrowed
package commands yourself, run each dependency level to completion before starting dependents.
- Manual parallel commands are only appropriate for read-only inspection or checks that are
demonstrably independent and do not clean, rebuild, package, install, or measure generated package
artifacts.
- For native, React Native, or E2E validation, use the implementation-specific runner documented in
the nearest `AGENTS.md`, package scripts, or README before deciding the test cannot run locally.
Missing attached devices, simulators, emulators, mock servers, or Metro are setup states; many
Expand Down
5 changes: 3 additions & 2 deletions implementations/web-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ This is a reference implementation for the
## What this demonstrates

Use this implementation when you need the smallest browser example for the Web SDK without a
framework layer. It demonstrates a static HTML integration, local mock API usage, Web SDK asset
copying, and Playwright coverage for browser-side optimization and tracking behavior.
framework layer. It demonstrates a static HTML integration, Web Components entry rendering, local
mock API usage, Web SDK asset copying, and Playwright coverage for browser-side optimization,
tracking, live updates, consent gating, and offline recovery behavior.

## CDA locale handling

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { expect, test } from '@playwright/test'
import { expect, test, type Locator, type Page } from '@playwright/test'

function getRenderedEntries(page: Page): Locator {
return page.locator('#auto-observed, #manually-observed')
}

test.describe('identified user', () => {
test.beforeEach(async ({ page }) => {
Expand All @@ -23,42 +27,50 @@ test.describe('identified user', () => {
})

test('displays common variants', async ({ page }) => {
const renderedEntries = getRenderedEntries(page)

await expect(
page.getByText(
renderedEntries.getByText(
'This is a merge tag content entry that displays the visitor\'s continent "EU" embedded within the text.',
),
).toBeVisible()

await expect(
page.getByText('This is a variant content entry for visitors from Europe.'),
renderedEntries.getByText('This is a variant content entry for visitors from Europe.'),
).toBeVisible()

await expect(
page.getByText('This is a variant content entry for visitors using a desktop browser.'),
renderedEntries.getByText(
'This is a variant content entry for visitors using a desktop browser.',
),
).toBeVisible()
})

test('displays identified user variants', async ({ page }) => {
await expect(page.getByText('This is a level 0 nested variant entry.')).toBeVisible()
const renderedEntries = getRenderedEntries(page)

await expect(page.getByText('This is a level 1 nested variant entry.')).toBeVisible()
await expect(renderedEntries.getByText('This is a level 0 nested variant entry.')).toBeVisible()

await expect(page.getByText('This is a level 2 nested variant entry.')).toBeVisible()
await expect(renderedEntries.getByText('This is a level 1 nested variant entry.')).toBeVisible()

await expect(renderedEntries.getByText('This is a level 2 nested variant entry.')).toBeVisible()

await expect(
page.getByText('This is a variant content entry for return visitors.'),
renderedEntries.getByText('This is a variant content entry for return visitors.'),
).toBeVisible()

await expect(
page.getByText('This is a variant content entry for an A/B/C experiment: B'),
renderedEntries.getByText('This is a variant content entry for an A/B/C experiment: B'),
).toBeVisible()

await expect(
page.getByText('This is a variant content entry for visitors with a custom event.'),
renderedEntries.getByText(
'This is a variant content entry for visitors with a custom event.',
),
).toBeVisible()

await expect(
page.getByText('This is a variant content entry for identified users.'),
renderedEntries.getByText('This is a variant content entry for identified users.'),
).toBeVisible()
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { expect, test } from '@playwright/test'
import { expect, test, type Locator, type Page } from '@playwright/test'

function getRenderedEntries(page: Page): Locator {
return page.locator('#auto-observed, #manually-observed')
}

test.describe('unidentified user', () => {
test.beforeEach(async ({ page }) => {
Expand All @@ -7,42 +11,58 @@ test.describe('unidentified user', () => {
})

test('displays common variants', async ({ page }) => {
const renderedEntries = getRenderedEntries(page)

await expect(
page.getByText(
renderedEntries.getByText(
'This is a merge tag content entry that displays the visitor\'s continent "EU" embedded within the text.',
),
).toBeVisible()

await expect(
page.getByText('This is a variant content entry for visitors from Europe.'),
renderedEntries.getByText('This is a variant content entry for visitors from Europe.'),
).toBeVisible()

await expect(
page.getByText('This is a variant content entry for visitors using a desktop browser.'),
renderedEntries.getByText(
'This is a variant content entry for visitors using a desktop browser.',
),
).toBeVisible()
})

test('displays unidentified user variants', async ({ page }) => {
await expect(page.getByText('This is a level 0 nested baseline entry.')).toBeVisible()
const renderedEntries = getRenderedEntries(page)

await expect(page.getByText('This is a level 1 nested baseline entry.')).toBeVisible()
await expect(
renderedEntries.getByText('This is a level 0 nested baseline entry.'),
).toBeVisible()

await expect(page.getByText('This is a level 2 nested baseline entry.')).toBeVisible()
await expect(
renderedEntries.getByText('This is a level 1 nested baseline entry.'),
).toBeVisible()

await expect(page.getByText('This is a variant content entry for new visitors.')).toBeVisible()
await expect(
renderedEntries.getByText('This is a level 2 nested baseline entry.'),
).toBeVisible()

await expect(
renderedEntries.getByText('This is a variant content entry for new visitors.'),
).toBeVisible()

await expect(
page.getByText('This is a variant content entry for an A/B/C experiment: B'),
renderedEntries.getByText('This is a variant content entry for an A/B/C experiment: B'),
).toBeVisible()

await expect(
page.getByText(
renderedEntries.getByText(
'This is a baseline content entry for all visitors with or without a custom event.',
),
).toBeVisible()

await expect(
page.getByText('This is a baseline content entry for all identified or unidentified users.'),
renderedEntries.getByText(
'This is a baseline content entry for all identified or unidentified users.',
),
).toBeVisible()
})
})
7 changes: 3 additions & 4 deletions implementations/web-sdk/e2e/entry-click-tracking.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const clickScenarios: ClickScenario[] = [
{
name: 'direct entry button',
entryTestId: 'entry-click-direct-entry',
clickTargetTestId: 'entry-click-direct-entry',
clickTargetTestId: 'content-4ib0hsHWoSOnCVdDkizE8d',
},
{
name: 'clickable descendant button',
Expand Down Expand Up @@ -65,8 +65,8 @@ test.describe('entry click tracking', () => {
page,
}) => {
for (const scenario of clickScenarios) {
const entryLocator = page.getByTestId(scenario.entryTestId)
await expect(entryLocator, `${scenario.name}: entry should render`).toBeVisible()
const target = page.getByTestId(scenario.clickTargetTestId)
await expect(target, `${scenario.name}: click target should render`).toBeVisible()

await expect
.poll(async () => await readResolvedEntryId(page, scenario.entryTestId), {
Expand All @@ -75,7 +75,6 @@ test.describe('entry click tracking', () => {
.not.toEqual('')

const resolvedEntryId = await readResolvedEntryId(page, scenario.entryTestId)
const target = page.getByTestId(scenario.clickTargetTestId)

await target.scrollIntoViewIfNeeded()
await target.click()
Expand Down
9 changes: 4 additions & 5 deletions implementations/web-sdk/e2e/entry-hover-tracking.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const hoverScenarios: HoverScenario[] = [
{
name: 'direct entry button',
entryTestId: 'entry-click-direct-entry',
hoverTargetTestId: 'entry-click-direct-entry',
hoverTargetTestId: 'content-4ib0hsHWoSOnCVdDkizE8d',
},
{
name: 'hoverable descendant button',
Expand All @@ -20,7 +20,7 @@ const hoverScenarios: HoverScenario[] = [
{
name: 'inline entry nested in clickable ancestor',
entryTestId: 'entry-click-ancestor-entry',
hoverTargetTestId: 'entry-click-ancestor-entry',
hoverTargetTestId: 'content-2Z2WLOx07InSewC3LUB3eX',
},
]

Expand Down Expand Up @@ -81,10 +81,9 @@ test.describe('entry hover tracking', () => {
const hoverButtons = getHoverButtons(page)

for (const scenario of hoverScenarios) {
const entryLocator = page.getByTestId(scenario.entryTestId)
await expect(entryLocator, `${scenario.name}: entry should render`).toBeVisible()

const target = page.getByTestId(scenario.hoverTargetTestId)
await expect(target, `${scenario.name}: hover target should render`).toBeVisible()

const baselineHoverEventCount = await hoverButtons.count()

await target.scrollIntoViewIfNeeded()
Expand Down
38 changes: 35 additions & 3 deletions implementations/web-sdk/e2e/entry-view-tracking.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test } from '@playwright/test'
import { expect, test, type Locator, type Page } from '@playwright/test'

const variantEntryTexts: Record<string, string> = {
'1JAU028vQ7v6nB2swl3NBo': 'This is a level 0 nested baseline entry.',
Expand All @@ -16,6 +16,12 @@ const variantEntryTexts: Record<string, string> = {
'This is a baseline content entry for all identified or unidentified users.',
}

const MANUAL_VIEW_BASELINE_ENTRY_ID = '5XHssysWUDECHzKLzoIsg1'

function getRenderedEntries(page: Page): Locator {
return page.locator('#auto-observed, #manually-observed')
}

test.describe('entry view tracking', () => {
test.describe('without consent', () => {
test.beforeEach(async ({ page }) => {
Expand All @@ -31,8 +37,10 @@ test.describe('entry view tracking', () => {
})

test('entry view events have not been emitted', async ({ page }) => {
const renderedEntries = getRenderedEntries(page)

for (const entryText of Object.values(variantEntryTexts)) {
const element = page.getByText(entryText)
const element = renderedEntries.getByText(entryText)

await element.scrollIntoViewIfNeeded()

Expand Down Expand Up @@ -60,12 +68,14 @@ test.describe('entry view tracking', () => {
})

test('entry view events have been emitted', async ({ page }) => {
const renderedEntries = getRenderedEntries(page)

for (const entryId of Object.keys(variantEntryTexts)) {
const entryText = variantEntryTexts[entryId]

if (!entryText) continue

const element = page.getByText(entryText)
const element = renderedEntries.getByText(entryText)

await element.scrollIntoViewIfNeeded()

Expand Down Expand Up @@ -97,5 +107,27 @@ test.describe('entry view tracking', () => {

expect(new Set(viewIds).size).toEqual(viewIds.length)
})

test('manual view example emits one view event without auto double tracking', async ({
page,
}) => {
const manualEntry = page.getByTestId('manual-view-entry')

await expect(manualEntry).toHaveAttribute('track-views', 'false')
await expect(manualEntry).toHaveAttribute('data-ctfl-track-views', 'false')
await expect(manualEntry).toHaveAttribute('data-ctfl-entry-id', /.+/)

const resolvedEntryId = await manualEntry.getAttribute('data-ctfl-entry-id')
expect(resolvedEntryId).not.toBeNull()

await page.getByTestId(`content-${MANUAL_VIEW_BASELINE_ENTRY_ID}`).scrollIntoViewIfNeeded()
await page.clock.fastForward('02:00')

await expect(
page.locator(
`#event-stream li button[data-component-id="${resolvedEntryId}"][data-view-id]`,
),
).toHaveCount(1)
})
})
})
41 changes: 41 additions & 0 deletions implementations/web-sdk/e2e/events-consent-gating.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type Page, expect, test } from '@playwright/test'

async function scrollThroughEntries(page: Page): Promise<void> {
const entries = page.locator('[data-testid^="content-"]')
const entryCount = await entries.count()

for (let index = 0; index < entryCount; index += 1) {
await entries.nth(index).scrollIntoViewIfNeeded()
}
}

test.describe('consent gating', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('domcontentloaded')
await expect(page.getByRole('heading', { name: 'Utilities' })).toBeVisible()
})

test('allows page events without consent but gates entry view events', async ({ page }) => {
const pageEvents = page.locator('[data-testid^="event-page-"]')
const viewEvents = page.locator('[data-testid^="event-view-"]')

await expect(pageEvents.first()).toBeVisible()

await scrollThroughEntries(page)
await expect(viewEvents).toHaveCount(0)
})

test('emits entry view events after consent is accepted', async ({ page }) => {
const pageEvents = page.locator('[data-testid^="event-page-"]')
const viewEvents = page.locator('[data-testid^="event-view-"]')

await expect(pageEvents.first()).toBeVisible()

await page.getByTestId('consent-button').click()
await expect(page.getByTestId('consent-status')).toHaveText('Consent: true')
await scrollThroughEntries(page)

await expect.poll(async () => await viewEvents.count()).toBeGreaterThan(0)
})
})
Loading
Loading