Skip to content

Commit

Permalink
Breakage reporting feature (#1006)
Browse files Browse the repository at this point in the history
  • Loading branch information
nshuba authored Aug 6, 2024
1 parent 05799cd commit 4ffda90
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 6 deletions.
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

0 comments on commit 4ffda90

Please sign in to comment.