Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breakage reporting feature #1006

Merged
merged 7 commits into from
Aug 6, 2024
Merged
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
126 changes: 126 additions & 0 deletions integration-test/playwright/breakage-reporting.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { test, expect } from '@playwright/test'
import { readFileSync } from 'fs'
import {
mockWindowsMessaging,
readOutgoingMessages, simulateSubscriptionMessage, waitForCallCount,
wrapWindowsScripts
} from '@duckduckgo/messaging/lib/test-utils.mjs'
import { perPlatform } from './type-helpers.mjs'

test('Breakage Reporting Feature', async ({ page }, testInfo) => {
const breakageFeature = BreakageReportingSpec.create(page, testInfo)
await breakageFeature.enabled()
await breakageFeature.navigate()

await page.evaluate(simulateSubscriptionMessage, {
messagingContext: {
context: 'contentScopeScripts',
featureName: 'breakageReporting',
env: 'development'
},
name: 'getBreakageReportValues',
payload: {},
injectName: breakageFeature.build.name
})

await page.waitForFunction(waitForCallCount, {
method: 'breakageReportResult',
count: 1
}, { timeout: 5000, polling: 100 })
const calls = await page.evaluate(readOutgoingMessages)
expect(calls.length).toBe(1)

const result = calls[0].payload.params
expect(result.jsPerformance.length).toBe(1)
expect(result.jsPerformance[0]).toBeGreaterThan(0)
expect(result.referrer).toBe('http://localhost:3220/breakage-reporting/index.html')
})

export class BreakageReportingSpec {
htmlPage = '/breakage-reporting/index.html'
config = './integration-test/test-pages/breakage-reporting/config/config.json'
/**
* @param {import("@playwright/test").Page} page
* @param {import("./type-helpers.mjs").Build} build
* @param {import("./type-helpers.mjs").PlatformInfo} platform
*/
constructor (page, build, platform) {
this.page = page
this.build = build
this.platform = platform
}

async enabled () {
const config = JSON.parse(readFileSync(this.config, 'utf8'))
await this.setup({ config })
}

async navigate () {
await this.page.goto(this.htmlPage)

await this.page.evaluate(() => {
window.location.href = '/breakage-reporting/pages/ref.html'
})
await this.page.waitForURL('**/ref.html')

// Wait for first paint event to ensure we can get the performance metrics
await this.page.evaluate(() => {
const response = new Promise((resolve) => {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.name === 'first-paint') {
observer.disconnect()
// @ts-expect-error - error TS2810: Expected 1 argument, but got 0. 'new Promise()' needs a JSDoc hint to produce a 'resolve' that can be called without arguments.
resolve()
}
})
})

observer.observe({ type: 'paint', buffered: true })
})
return response
})
}

/**
* @param {object} params
* @param {Record<string, any>} params.config
* @return {Promise<void>}
*/
async setup (params) {
const { config } = params

// read the built file from disk and do replacements
const injectedJS = wrapWindowsScripts(this.build.artifact, {
$CONTENT_SCOPE$: config,
$USER_UNPROTECTED_DOMAINS$: [],
$USER_PREFERENCES$: {
platform: { name: 'windows' },
debug: true
}
})

await this.page.addInitScript(mockWindowsMessaging, {
messagingContext: {
env: 'development',
context: 'contentScopeScripts',
featureName: 'n/a'
},
responses: {}
})

// attach the JS
await this.page.addInitScript(injectedJS)
}

/**
* Helper for creating an instance per platform
* @param {import("@playwright/test").Page} page
* @param {import("@playwright/test").TestInfo} testInfo
*/
static create (page, testInfo) {
// Read the configuration object to determine which platform we're testing against
const { platformInfo, build } = perPlatform(testInfo.project.use)
return new BreakageReportingSpec(page, build, platformInfo)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"unprotectedTemporary": [],
"features": {
"breakageReporting": {
"state": "enabled",
"exceptions": []
}
}
}
11 changes: 11 additions & 0 deletions integration-test/test-pages/breakage-reporting/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Breakage Reporting</title>
</head>
<body>
<p>Breakage Reporting</p>
</body>
</html>
11 changes: 11 additions & 0 deletions integration-test/test-pages/breakage-reporting/pages/ref.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Breakage Reporting with Referrer</title>
</head>
<body>
<p>Breakage Reporting with Referrer</p>
</body>
</html>
3 changes: 2 additions & 1 deletion playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export default defineConfig({
'integration-test/playwright/duckplayer-remote-config.spec.js',
'integration-test/playwright/harmful-apis.spec.js',
'integration-test/playwright/windows-permissions.spec.js',
'integration-test/playwright/broker-protection.spec.js'
'integration-test/playwright/broker-protection.spec.js',
'integration-test/playwright/breakage-reporting.spec.js'
],
use: { injectName: 'windows', platform: 'windows' }
},
Expand Down
6 changes: 4 additions & 2 deletions src/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const otherFeatures = /** @type {const} */([
'webCompat',
'windowsPermissionUsage',
'brokerProtection',
'performanceMetrics'
'performanceMetrics',
'breakageReporting'
])

/** @typedef {baseFeatures[number]|otherFeatures[number]} FeatureName */
Expand All @@ -48,7 +49,8 @@ export const platformSupport = {
...baseFeatures,
'windowsPermissionUsage',
'duckPlayer',
'brokerProtection'
'brokerProtection',
'breakageReporting'
],
firefox: [
'cookie',
Expand Down
16 changes: 16 additions & 0 deletions src/features/breakage-reporting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import ContentFeature from '../content-feature'
import { getJsPerformanceMetrics } from './breakage-reporting/utils.js'

export default class BreakageReporting extends ContentFeature {
init () {
this.messaging.subscribe('getBreakageReportValues', () => {
const jsPerformance = getJsPerformanceMetrics()
const referrer = document.referrer

this.messaging.notify('breakageReportResult', {
jsPerformance,
referrer
})
})
}
}
8 changes: 8 additions & 0 deletions src/features/breakage-reporting/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @returns array of performance metrics
*/
export function getJsPerformanceMetrics () {
const paintResources = performance.getEntriesByType('paint')
const firstPaint = paintResources.find((entry) => entry.name === 'first-contentful-paint')
return firstPaint ? [firstPaint.startTime] : []
}
5 changes: 2 additions & 3 deletions src/features/performance-metrics.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import ContentFeature from '../content-feature'
import { getJsPerformanceMetrics } from './breakage-reporting/utils.js'

export default class PerformanceMetrics extends ContentFeature {
init () {
this.messaging.subscribe('getVitals', () => {
const paintResources = performance.getEntriesByType('paint')
const firstPaint = paintResources.find((entry) => entry.name === 'first-contentful-paint')
const vitals = firstPaint ? [firstPaint.startTime] : []
const vitals = getJsPerformanceMetrics()
this.messaging.notify('vitalsResult', { vitals })
})
}
Expand Down
Loading