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
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
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 { SHOPIFY_STORE_URL, unlockShopifyStorePassword } from './shopify'
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 = SHOPIFY_STORE_URL
})
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
81 changes: 63 additions & 18 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 @@ -304,22 +305,40 @@ export function microfrontendSetup(options: SetupOptions, servers: Servers) {
})
}

// Salesforce apps don't serve a locally-generated page body; this factory only drives the
// page-side setup needed to init RUM on the remote Salesforce page.
// Salesforce and Shopify apps don't serve a locally-generated page body; this factory only
// intercepts the remote bundle request and injects the RUM configuration read by the app's
// own init snippet/component, via globals set on every new document
async function interceptRemoteBundleAndConfigureRum(
page: Page,
urlPattern: RegExp,
resolveFilePath: (url: string) => string,
options: SetupOptions,
servers: Servers,
headers?: Record<string, string>
): Promise<void> {
await page.route(urlPattern, async (route) => {
await route.fulfill({
body: await readFile(resolveFilePath(route.request().url())),
contentType: 'application/javascript',
headers,
})
})

if (options.rum) {
await page.addInitScript(
`window.RUM_CONFIGURATION = ${formatConfiguration(options.rum, servers)}
window.RUM_CONTEXT = ${JSON.stringify(options.context)}`
)
}
}

export async function salesforceSetup(options: SetupOptions, servers: Servers, page: Page): Promise<string> {
const salesforceAppDirectory = options.salesforceApp === 'experience-cloud' ? 'sf-experience-app' : 'sf-lwc-app'
const salesforceBundlePath = resolve(
__dirname,
`../../../apps/${salesforceAppDirectory}/force-app/main/default/staticresources/datadog_rum_salesforce.js`
)

await page.route(/\/resource(?:\/[^/?#]+)?\/datadog_rum_salesforce(?:\.js)?(?:[/?#].*)?$/, async (route) => {
await route.fulfill({
body: await readFile(salesforceBundlePath),
contentType: 'application/javascript',
})
})

if (options.salesforceApp === 'lwc') {
const { accessToken, instanceUrl } = await getSalesforceLwcSession()
await page.context().addCookies([
Expand All @@ -331,15 +350,41 @@ export async function salesforceSetup(options: SetupOptions, servers: Servers, p
])
}

if (options.rum) {
// Both sf-lwc-app and sf-experience-app have a committed datadogInit LWC that reads
// these globals and calls DD_RUM.init. On experience-cloud, that component only runs
// when the page is loaded with init=true.
await page.addInitScript(
`window.RUM_CONFIGURATION = ${formatConfiguration(options.rum, servers)}
window.RUM_CONTEXT = ${JSON.stringify(options.context)}`
)
}
// Both sf-lwc-app and sf-experience-app have a committed datadogInit LWC that reads the
// injected globals and calls DD_RUM.init. On experience-cloud, that component only runs
// when the page is loaded with init=true.
await interceptRemoteBundleAndConfigureRum(
page,
/\/resource(?:\/[^/?#]+)?\/datadog_rum_salesforce(?:\.js)?(?:[/?#].*)?$/,
() => salesforceBundlePath,
options,
servers
)
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)(?:[?#].*)?$/

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 interceptRemoteBundleAndConfigureRum(
page,
SHOPIFY_ASSET_URL_PATTERN,
(url) => {
const [, chunksSegment, fileName] = SHOPIFY_ASSET_URL_PATTERN.exec(url) || []
return resolve(shopifyBundleDir, chunksSegment || '', fileName)
},
options,
servers,
// The snippets load the script with `crossOrigin = 'anonymous'`, so the browser enforces
// CORS on this response even though it never leaves the machine.
{ 'access-control-allow-origin': '*' }
)
return ''
}

Expand Down
19 changes: 19 additions & 0 deletions test/e2e/lib/framework/shopify.ts

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: this file is not just about shopify urls. Maybe rename it to shopify.ts or shopifyUtils.ts?

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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.
export const SHOPIFY_STORE_URL = 'https://custom-pixel-e2e.myshopify.com/'

// 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))
}
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