Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ BS_ACCESS_KEY=xxx
SF_LWC_CLIENT_ID=xxx
SF_LWC_USERNAME=xxx
SF_LWC_INSTANCE_URL=xxx
SF_LWC_JWT_PRIVATE_KEY_B64=xxx
SF_LWC_JWT_PRIVATE_KEY_B64=xxx

# Shopify credentials
SHOPIFY_STORE_PASSWORD=xxx
27 changes: 26 additions & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ e2e:
- yarn build:apps
# Browsers are pre-installed in the CI image. If playwright (current or pinned 1.40.1)
# is upgraded without rebuilding the image, this job will crash — rebuild the image to fix it.
- FORCE_COLOR=1 PW_BROWSER=$BROWSER yarn test:e2e --project=$BROWSER
# Shopify is excluded here: it only runs on a schedule (see e2e-shopify-scheduled)
- FORCE_COLOR=1 PW_BROWSER=$BROWSER yarn test:e2e --project=$BROWSER --grep-invert "shopify"
after_script:
- node ./scripts/test/export-test-result.ts e2e

Expand Down Expand Up @@ -633,6 +634,30 @@ api-performance-benchmark:
- yarn
- node ./scripts/api-performance/index.ts

########################################################################################################################
# Shopify e2e (scheduled, against a live external store)
########################################################################################################################

e2e-shopify-scheduled:
stage: task
extends:
- .base-configuration
- .resource-allocation-4-cpus
only:
variables:
- $TARGET_TASK_NAME == "e2e-shopify-scheduled"
artifacts:
when: always
reports:
junit: test-report/e2e/*.xml
script:
- yarn
- yarn build
- yarn build:apps
- FORCE_COLOR=1 PW_BROWSER=chromium yarn test:e2e --project=chromium -g "shopify"
after_script:
- node ./scripts/test/export-test-result.ts e2e

########################################################################################################################
# Check expired telemetry
########################################################################################################################
Expand Down
4 changes: 4 additions & 0 deletions scripts/lib/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ export function getSfLwcJwtPrivateKey(): string {
return process.env.SF_LWC_JWT_PRIVATE_KEY_B64 ?? getSecretKey('ci.browser-sdk.sf_lwc_jwt_private_key_b64')
}

export function getShopifyStorePassword(): string {
return process.env.SHOPIFY_STORE_PASSWORD ?? getSecretKey('ci.browser-sdk.shopify_store_password')
}

function getSecretKey(name: string): string {
return command`
aws ssm get-parameter --region=us-east-1 --with-decryption --query=Parameter.Value --out=text --name=${name}
Expand Down
23 changes: 23 additions & 0 deletions test/e2e/lib/framework/buildShopifyUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Page } from '@playwright/test'
import { getShopifyStorePassword } from '../../../../scripts/lib/secrets.ts'

// A Datadog-owned dev store, password-protected, used only to exercise browser-rum-shopify
// against a real storefront + checkout + Custom Pixel sandbox.
const SHOPIFY_STORE_URL = 'https://custom-pixel-e2e.myshopify.com/'

export function buildShopifyUrl(): string {
return SHOPIFY_STORE_URL
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe since it's a constant we don't need this?


// Dev stores gate every page behind a storefront password until unlocked for the session.
const PASSWORD_PATH = /\/password\/?$/

export async function unlockShopifyStorePassword(page: Page): Promise<void> {
if (!PASSWORD_PATH.test(new URL(page.url()).pathname)) {
return
}

await page.getByRole('textbox', { name: /password/i }).fill(getShopifyStorePassword())
await page.getByRole('button', { name: /enter/i }).click()
await page.waitForURL((url) => !PASSWORD_PATH.test(url.pathname))
}
30 changes: 28 additions & 2 deletions test/e2e/lib/framework/createTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,21 @@ import {
} from '../helpers/playwright'
import { buildSalesforceUrl } from './buildSalesforceUrl'
import type { SalesforceApp } from './buildSalesforceUrl'
import { buildShopifyUrl, unlockShopifyStorePassword } from './buildShopifyUrl'
import { IntakeRegistry } from './intakeRegistry'
import { flushEvents } from './flushEvents'
import type { Servers } from './httpServers'
import { getTestServers, waitForServersIdle } from './httpServers'
import type { CallerLocation, EventBridgeOptions, SetupFactory, SetupOptions, UrlHook } from './pageSetups'
import { html, DEFAULT_SETUPS, npmSetup, appSetup, formatConfiguration, salesforceSetup } from './pageSetups'
import {
html,
DEFAULT_SETUPS,
npmSetup,
appSetup,
formatConfiguration,
salesforceSetup,
shopifySetup,
} from './pageSetups'
import { createDatadogHttpApi } from './serverApps/datadogHttpApi'
import type { DatadogHttpApiControl } from './serverApps/datadogHttpApi'
import { createMockServerApp } from './serverApps/mock'
Expand Down Expand Up @@ -116,6 +125,7 @@ class TestBuilder {
} = {}
private worker: Worker | undefined
private salesforceApp: SalesforceApp | undefined = undefined
private shopifyApp = false

constructor(
private title: string,
Expand Down Expand Up @@ -283,6 +293,15 @@ class TestBuilder {
return this
}

withShopifyApp() {
this.shopifyApp = true
this.setups = [{ factory: shopifySetup }]
this.baseUrlHooks.push((baseUrl) => {
baseUrl.href = buildShopifyUrl()
})
return this
}

withHostName(hostName: string) {
this.baseUrlHooks.push((baseUrl) => {
baseUrl.hostname = hostName
Expand Down Expand Up @@ -313,6 +332,7 @@ class TestBuilder {
callerLocation: this.callerLocation,
mockClock: this.mockClock,
salesforceApp: this.salesforceApp,
shopifyApp: this.shopifyApp,
}

if (this.alsoRunWithRumSlim) {
Expand Down Expand Up @@ -534,7 +554,7 @@ function createTestContext(

async function setUpTest(
browserLogsManager: BrowserLogsManager,
{ mockClock }: SetupOptions,
{ mockClock, shopifyApp }: SetupOptions,
{ baseUrl, page, browserContext }: TestContext
) {
browserContext.on('console', (msg) => {
Expand All @@ -543,6 +563,7 @@ async function setUpTest(
message: msg.text(),
source: 'console',
timestamp: Date.now(),
url: msg.location().url,
})
})

Expand All @@ -563,6 +584,11 @@ async function setUpTest(
}
}
await page.goto(baseUrl)

if (shopifyApp) {
await unlockShopifyStorePassword(page)
}

await waitForServersIdle()
}

Expand Down
36 changes: 36 additions & 0 deletions test/e2e/lib/framework/pageSetups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface SetupOptions {
callerLocation?: CallerLocation
mockClock: boolean
salesforceApp: SalesforceApp | undefined
shopifyApp: boolean
}

export interface CallerLocation {
Expand Down Expand Up @@ -343,6 +344,41 @@ export async function salesforceSetup(options: SetupOptions, servers: Servers, p
return ''
}

// Matches the CDN URL used by the store's Theme Liquid snippet and Custom Pixel for the main
// bundle and its dynamically-imported chunks (e.g. the session replay recorder), served from
// `https://www.datadoghq-browser-agent.com/<site>/v<major>/[chunks/]<name->]datadog-rum-shopify.js`
const SHOPIFY_ASSET_URL_PATTERN =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay not related specifically to your PR but how about making a function to reuse with Salesforce? Since it's the same pattern except for datadog-rum-shopify vs datadog_rum_salesforce?

/datadoghq-browser-agent\.com\/[^/]+\/v\d+\/(chunks\/)?([\w-]*datadog-rum-shopify\.js)(?:[?#].*)?$/

// Shopify apps don't serve a locally-generated page body; this factory only intercepts the
// bootstrap script request and injects the RUM configuration read by the store's Theme Liquid
// snippet and Custom Pixel.
export async function shopifySetup(options: SetupOptions, servers: Servers, page: Page): Promise<string> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: those setup functions are templates to render an HTML page the e2e test will load. It seems like you are doing very different things here. What about moving this logic in shopify.scenario.ts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was following the example of the Salesforce, which has similar setup pattern

It seems like you are doing very different things here

Actually not that different - the part of rendering an HTML page for e2e test is injecting the local sdk version and passing the init options for it - and this is exactly what I'm doing here. This method doesn't render the html - it's rendered by predefined Shopify dev-store - but I still need to override the sdk bundle path and pass the init config for it. Therefore I believe it's the right place to do this, it would look overly cumbersome in shopify.scenario.ts - but let me know if you disagree, I'm open to the discussion :)

const shopifyBundleDir = resolve(__dirname, '../../../../packages/browser-rum-shopify/bundle')

await page.route(SHOPIFY_ASSET_URL_PATTERN, async (route) => {
const [, chunksSegment, fileName] = SHOPIFY_ASSET_URL_PATTERN.exec(route.request().url()) || []
const filePath = resolve(shopifyBundleDir, chunksSegment || '', fileName)
await route.fulfill({
body: await readFile(filePath),
contentType: 'application/javascript',
// The snippets load the script with `crossOrigin = 'anonymous'`, so the browser enforces
// CORS on this response even though it never leaves the machine.
headers: { 'access-control-allow-origin': '*' },
})
})

if (options.rum) {
// addInitScript runs on every new document in the page, including the Custom Pixel's
// sandboxed iframe, so this reaches both the storefront and checkout injection points.
await page.addInitScript(
`window.RUM_CONFIGURATION = ${formatConfiguration(options.rum, servers)}
window.RUM_CONTEXT = ${JSON.stringify(options.context)}`
)
}
return ''
}

function basePage({ header, body, footer }: { header?: string; body?: string; footer?: string }) {
// prettier-ignore
// The empty favicon avoids a /favicon.ico request from the browser.
Expand Down
1 change: 1 addition & 0 deletions test/e2e/lib/helpers/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface BrowserLog {
message: string
source: string
timestamp: number
url?: string
}

const IGNORE_LOG_MESSAGES = [
Expand Down
Loading
Loading