Skip to content

Commit 1f70c27

Browse files
committed
PR feedback
1 parent a41337d commit 1f70c27

File tree

6 files changed

+154
-29
lines changed

6 files changed

+154
-29
lines changed

injected/integration-test/duckplayer-native.spec.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,13 @@ test.describe('Duck Player Native custom error view', () => {
170170
await duckPlayer.didShowUnknownError();
171171
});
172172
});
173+
174+
test.describe('Reporting exceptions', () => {
175+
test('initial setup error', async ({ page }, workerInfo) => {
176+
const duckPlayer = DuckPlayerNative.create(page, workerInfo);
177+
await duckPlayer.withRemoteConfig();
178+
await duckPlayer.messagingError();
179+
await duckPlayer.gotoBlankPage();
180+
await duckPlayer.didSendMessagingException();
181+
});
182+
});

injected/integration-test/page-objects/duckplayer-native.js

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ import { ResultsCollector } from './results-collector.js';
1111
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1212
const configFiles = /** @type {const} */ (['native.json']);
1313

14-
const defaultInitialSetup = {
14+
const featureName = 'duckPlayerNative';
15+
16+
/** @type {PageType} */
17+
const DEFAULT_PAGE_TYPE = 'YOUTUBE';
18+
19+
/** @type {import('../../src/features/duck-player-native.js').InitialSettings} */
20+
const DEFAULT_INITIAL_SETUP = {
1521
locale: 'en',
1622
};
1723

18-
const featureName = 'duckPlayerNative';
19-
2024
export class DuckPlayerNative {
2125
/** @type {Partial<Record<PageType, string>>} */
2226
pages = {
@@ -36,7 +40,7 @@ export class DuckPlayerNative {
3640
this.isMobile = platform.name === 'android' || platform.name === 'ios';
3741
this.collector = new ResultsCollector(page, build, platform);
3842
this.collector.withMockResponse({
39-
initialSetup: defaultInitialSetup,
43+
initialSetup: DEFAULT_INITIAL_SETUP,
4044
onCurrentTimestamp: {},
4145
});
4246
this.collector.withUserPreferences({
@@ -86,13 +90,22 @@ export class DuckPlayerNative {
8690
}
8791

8892
/**
89-
* @param {PageType} pageType
93+
* Goes to the default page (YOUTUBE) without overriding initialSetup
94+
* Useful for testing messaging errors
95+
**/
96+
async gotoBlankPage() {
97+
await this.gotoPage();
98+
}
99+
100+
/**
101+
* @param {PageType} [pageType]
90102
* @param {object} [params]
91103
* @param {PlayerPageVariants} [params.variant]
92104
* @param {string} [params.videoID]
93105
*/
94106
async gotoPage(pageType, params = {}) {
95-
await this.withPageType(pageType);
107+
if (pageType) await this.withPageType(pageType);
108+
const page = this.pages[pageType || DEFAULT_PAGE_TYPE];
96109

97110
const defaultVariant = this.isMobile ? 'mobile' : 'default';
98111
const { variant = defaultVariant, videoID = '123' } = params;
@@ -101,8 +114,6 @@ export class DuckPlayerNative {
101114
['variant', variant],
102115
]);
103116

104-
const page = this.pages[pageType];
105-
106117
await this.page.goto(page + '?' + urlParams.toString());
107118
}
108119

@@ -122,7 +133,7 @@ export class DuckPlayerNative {
122133
* @return {Promise<void>}
123134
*/
124135
async withPageType(pageType) {
125-
const initialSetup = this.collector.mockResponses?.initialSetup || defaultInitialSetup;
136+
const initialSetup = this.collector.mockResponses?.initialSetup || DEFAULT_INITIAL_SETUP;
126137

127138
await this.collector.updateMockResponse({
128139
initialSetup: { pageType, ...initialSetup },
@@ -134,7 +145,7 @@ export class DuckPlayerNative {
134145
* @return {Promise<void>}
135146
*/
136147
async withPlaybackPaused(playbackPaused = true) {
137-
const initialSetup = this.collector.mockResponses.initialSetup || defaultInitialSetup;
148+
const initialSetup = this.collector.mockResponses.initialSetup || DEFAULT_INITIAL_SETUP;
138149

139150
await this.collector.updateMockResponse({
140151
initialSetup: { playbackPaused, ...initialSetup },
@@ -206,6 +217,34 @@ export class DuckPlayerNative {
206217
await this.simulateSubscriptionMessage('onUrlChanged', { pageType });
207218
}
208219

220+
/**
221+
* Simulates a messaging error by passing an empty initialSetup object
222+
*/
223+
async messagingError() {
224+
await this.build.switch({
225+
android: async () => {
226+
await this.collector.updateMockResponse({
227+
initialSetup: {},
228+
});
229+
},
230+
apple: async () => {
231+
await this.collector.updateMockResponse({
232+
initialSetup: null,
233+
});
234+
},
235+
'apple-isolated': async () => {
236+
await this.collector.updateMockResponse({
237+
initialSetup: null,
238+
});
239+
},
240+
windows: async () => {
241+
await this.collector.updateMockResponse({
242+
initialSetup: '',
243+
});
244+
},
245+
});
246+
}
247+
209248
/* Messaging assertions */
210249

211250
async didSendInitialHandshake() {
@@ -236,6 +275,42 @@ export class DuckPlayerNative {
236275
]);
237276
}
238277

278+
/**
279+
* @param {string} kind
280+
* @param {string} message
281+
*/
282+
async didSendException(kind, message, context = 'contentScopeScripts') {
283+
const messages = await this.collector.waitForMessage('reportMetric');
284+
expect(messages).toMatchObject([
285+
{
286+
payload: {
287+
context,
288+
featureName: 'duckPlayerNative',
289+
method: 'reportMetric',
290+
params: { metricName: 'exception', params: { kind, message } },
291+
},
292+
},
293+
]);
294+
}
295+
296+
async didSendMessagingException() {
297+
await this.build.switch({
298+
android: async () => {
299+
// Android produces a TypeError due to how its messaging lib is wired up
300+
await this.didSendException('TypeError', "Cannot read properties of undefined (reading 'privatePlayerMode')");
301+
},
302+
apple: async () => {
303+
await this.didSendException('MessagingError', 'an unknown error occurred');
304+
},
305+
'apple-isolated': async () => {
306+
await this.didSendException('MessagingError', 'an unknown error occurred', 'contentScopeScriptsIsolated');
307+
},
308+
windows: async () => {
309+
await this.didSendException('MessagingError', 'unknown error');
310+
},
311+
});
312+
}
313+
239314
/* Thumbnail Overlay assertions */
240315

241316
async didShowOverlay() {

injected/src/features/duck-player-native.js

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DuckPlayerNativeMessages } from './duckplayer-native/messages.js';
44
import { setupDuckPlayerForNoCookie, setupDuckPlayerForSerp, setupDuckPlayerForYouTube } from './duckplayer-native/sub-feature.js';
55
import { Environment } from './duckplayer/environment.js';
66
import { Logger } from './duckplayer/util.js';
7+
import { MetricsReporter, EXCEPTION_KIND_INITIAL_SETUP_ERROR } from '../../../special-pages/shared/metrics-reporter.js';
78

89
/**
910
* @import {DuckPlayerNativeSubFeature} from './duckplayer-native/sub-feature.js'
@@ -14,8 +15,8 @@ import { Logger } from './duckplayer/util.js';
1415
/**
1516
* @typedef InitialSettings - The initial payload used to communicate render-blocking information
1617
* @property {string} locale - UI locale
17-
* @property {UrlChangeSettings['pageType']} pageType - The type of the current page
18-
* @property {boolean} playbackPaused - Should video start playing or paused
18+
* @property {UrlChangeSettings['pageType']} [pageType] - The type of the current page
19+
* @property {boolean} [playbackPaused] - Should video start playing or paused
1920
*/
2021

2122
/**
@@ -29,30 +30,46 @@ export class DuckPlayerNativeFeature extends ContentFeature {
2930
/** @type {TranslationFn} */
3031
t;
3132

32-
async init(args) {
33+
init(args) {
3334
/**
3435
* This feature never operates in a frame
3536
*/
3637
if (isBeingFramed()) return;
3738

38-
const selectors = this.getFeatureSetting('selectors');
39-
if (!selectors) {
40-
console.warn('No selectors found. Check remote config. Feature will not be initialized.');
41-
return;
42-
}
43-
4439
const locale = args?.locale || args?.language || 'en';
4540
const env = new Environment({
4641
debug: this.isDebug,
4742
injectName: import.meta.injectName,
4843
platform: this.platform,
4944
locale,
5045
});
46+
const metrics = new MetricsReporter(this.messaging);
47+
48+
try {
49+
const selectors = this.getFeatureSetting('selectors');
50+
if (!selectors) {
51+
console.warn('No selectors found. Check remote config. Feature will not be initialized.');
52+
return;
53+
}
5154

52-
// Translation function to be used by view components
53-
this.t = (key) => env.strings('native.json')[key];
55+
// Translation function to be used by view components
56+
this.t = (key) => env.strings('native.json')[key];
57+
58+
const messages = new DuckPlayerNativeMessages(this.messaging, env);
59+
this.initDuckPlayerNative(messages, selectors, env)
60+
// Using then instead of await because this is the public interface of the parent, which doesn't explicitly wait for promises to be resolved.
61+
// eslint-disable-next-line promise/prefer-await-to-then
62+
.catch((e) => {
63+
console.error(e);
64+
metrics.reportExceptionWithError(e);
65+
});
66+
} catch (e) {
67+
console.error(e);
68+
metrics.reportExceptionWithError(e);
69+
}
70+
}
5471

55-
const messages = new DuckPlayerNativeMessages(this.messaging, env);
72+
async initDuckPlayerNative(messages, selectors, env) {
5673
messages.subscribeToURLChange(({ pageType }) => {
5774
const playbackPaused = false; // This can be added to the event data in the future if needed
5875
this.urlDidChange(pageType, selectors, playbackPaused, env, messages);
@@ -64,7 +81,14 @@ export class DuckPlayerNativeFeature extends ContentFeature {
6481
try {
6582
initialSetup = await messages.initialSetup();
6683
} catch (e) {
67-
console.warn('Failed to get initial setup', e);
84+
console.warn(e);
85+
return;
86+
}
87+
88+
if (!initialSetup) {
89+
const message = 'InitialSetup data is missing';
90+
console.warn(message);
91+
messages.metrics.reportException({ message, kind: EXCEPTION_KIND_INITIAL_SETUP_ERROR });
6892
return;
6993
}
7094

@@ -118,7 +142,7 @@ export class DuckPlayerNativeFeature extends ContentFeature {
118142
if (document.readyState === 'loading') {
119143
const loadHandler = () => {
120144
logger.log('Running deferred load handlers');
121-
nextPage.onLoad();
145+
nextPage?.onLoad();
122146
messages.notifyScriptIsReady();
123147
};
124148
document.addEventListener('DOMContentLoaded', loadHandler, { once: true });

injected/src/features/duck-player.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ import { MetricsReporter } from '../../../special-pages/shared/metrics-reporter.
6161
* @internal
6262
*/
6363
export default class DuckPlayerFeature extends ContentFeature {
64-
async init(args) {
64+
init(args) {
6565
/**
6666
* This feature never operates in a frame
6767
*/
@@ -93,6 +93,7 @@ export default class DuckPlayerFeature extends ContentFeature {
9393
if (!this.messaging) {
9494
throw new Error('cannot operate duck player without a messaging backend');
9595
}
96+
const metrics = new MetricsReporter(this.messaging);
9697

9798
try {
9899
const locale = args?.locale || args?.language || 'en';
@@ -105,12 +106,18 @@ export default class DuckPlayerFeature extends ContentFeature {
105106
const comms = new DuckPlayerOverlayMessages(this.messaging, env);
106107

107108
if (overlaysEnabled) {
108-
await initOverlays(overlaySettings.youtube, env, comms);
109+
initOverlays(overlaySettings.youtube, env, comms)
110+
// Using then instead of await because this is the public interface of the parent, which doesn't explicitly wait for promises to be resolved.
111+
// eslint-disable-next-line promise/prefer-await-to-then
112+
.catch((e) => {
113+
console.error(e);
114+
metrics.reportExceptionWithError(e);
115+
});
109116
} else if (serpProxyEnabled) {
110117
comms.serpProxy();
111118
}
112119
} catch (e) {
113-
const metrics = new MetricsReporter(this.messaging);
120+
console.error(e);
114121
metrics.reportExceptionWithError(e);
115122
}
116123
}

injected/src/features/duckplayer-native/messages.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as constants from './constants.js';
2+
import { MetricsReporter, EXCEPTION_KIND_MESSAGING_ERROR } from '../../../../special-pages/shared/metrics-reporter.js';
23

34
/** @import {YouTubeError} from './error-detection.js' */
45
/** @import {Environment} from '../duckplayer/environment.js' */
@@ -42,13 +43,20 @@ export class DuckPlayerNativeMessages {
4243
*/
4344
this.messaging = messaging;
4445
this.environment = environment;
46+
this.metrics = new MetricsReporter(messaging);
4547
}
4648

4749
/**
4850
* @returns {Promise<import('../duck-player-native.js').InitialSettings>}
4951
*/
50-
initialSetup() {
51-
return this.messaging.request(constants.MSG_NAME_INITIAL_SETUP);
52+
async initialSetup() {
53+
try {
54+
console.log('initialSetup');
55+
return await this.messaging.request(constants.MSG_NAME_INITIAL_SETUP);
56+
} catch (e) {
57+
this.metrics.reportException({ message: e?.message, kind: EXCEPTION_KIND_MESSAGING_ERROR });
58+
throw e;
59+
}
5260
}
5361

5462
/**

special-pages/shared/metrics-reporter.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export class MetricsReporter {
157157
* ```
158158
*/
159159
reportException(params) {
160+
console.log('reportException', params);
160161
const metric = this._createExceptionMetric(params);
161162
this._sendMetric(metric);
162163
}

0 commit comments

Comments
 (0)