Skip to content

Commit 22e3eb0

Browse files
shakyShaneShane Osbourne
andauthored
adding debugging+performance helpers (#1247)
* adding debugging+performance helpers * linting --------- Co-authored-by: Shane Osbourne <[email protected]>
1 parent 3e17e6f commit 22e3eb0

File tree

7 files changed

+277
-17
lines changed

7 files changed

+277
-17
lines changed

special-pages/pages/new-tab/app/favorites/components/FavoritesCustomized.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { h } from 'preact';
22
import { useContext } from 'preact/hooks';
33

4-
import { useTypedTranslation } from '../../types.js';
4+
import { useTelemetry, useTypedTranslation } from '../../types.js';
55
import { useVisibility } from '../../widget-list/widget-config.provider.js';
66
import { useCustomizer } from '../../customizer/Customizer.js';
77

@@ -14,8 +14,10 @@ import { FavoritesMemo } from './Favorites.js';
1414
*/
1515
export function FavoritesConsumer() {
1616
const { state, toggle, favoritesDidReOrder, openContextMenu, openFavorite, add } = useContext(FavoritesContext);
17+
const telemetry = useTelemetry();
1718

1819
if (state.status === 'ready') {
20+
telemetry.measureFromPageLoad('favorites-will-render', 'time to favorites');
1921
return (
2022
<PragmaticDND items={state.data.favorites} itemsDidReOrder={favoritesDidReOrder}>
2123
<FavoritesMemo

special-pages/pages/new-tab/app/index.js

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { EnvironmentProvider, UpdateEnvironment } from '../../../shared/componen
44
import { Fallback } from '../../../shared/components/Fallback/Fallback.jsx';
55
import { ErrorBoundary } from '../../../shared/components/ErrorBoundary.js';
66
import { SettingsProvider } from './settings.provider.js';
7-
import { InitialSetupContext, MessagingContext } from './types';
7+
import { InitialSetupContext, MessagingContext, TelemetryContext } from './types';
88
import { TranslationProvider } from '../../../shared/components/TranslationsProvider.js';
99
import { WidgetConfigService } from './widget-list/widget-config.service.js';
1010
import enStrings from '../src/locales/en/newtab.json';
@@ -15,9 +15,10 @@ import { widgetEntryPoint } from './widget-list/WidgetList.js';
1515

1616
/**
1717
* @param {import("../src/js").NewTabPage} messaging
18+
* @param {import("./telemetry/telemetry.js").Telemetry} telemetry
1819
* @param {import("../../../shared/environment").Environment} baseEnvironment
1920
*/
20-
export async function init(messaging, baseEnvironment) {
21+
export async function init(messaging, telemetry, baseEnvironment) {
2122
const init = await messaging.init();
2223

2324
if (!Array.isArray(init.widgets)) {
@@ -107,18 +108,20 @@ export async function init(messaging, baseEnvironment) {
107108
<UpdateEnvironment search={window.location.search} />
108109
<MessagingContext.Provider value={messaging}>
109110
<InitialSetupContext.Provider value={init}>
110-
<SettingsProvider settings={settings}>
111-
<TranslationProvider translationObject={strings} fallback={strings} textLength={environment.textLength}>
112-
<WidgetConfigProvider
113-
api={widgetConfigAPI}
114-
widgetConfigs={init.widgetConfigs}
115-
widgets={init.widgets}
116-
entryPoints={entryPoints}
117-
>
118-
<App />
119-
</WidgetConfigProvider>
120-
</TranslationProvider>
121-
</SettingsProvider>
111+
<TelemetryContext.Provider value={telemetry}>
112+
<SettingsProvider settings={settings}>
113+
<TranslationProvider translationObject={strings} fallback={strings} textLength={environment.textLength}>
114+
<WidgetConfigProvider
115+
api={widgetConfigAPI}
116+
widgetConfigs={init.widgetConfigs}
117+
widgets={init.widgets}
118+
entryPoints={entryPoints}
119+
>
120+
<App />
121+
</WidgetConfigProvider>
122+
</TranslationProvider>
123+
</SettingsProvider>
124+
</TelemetryContext.Provider>
122125
</InitialSetupContext.Provider>
123126
</MessagingContext.Provider>
124127
</ErrorBoundary>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { h } from 'preact';
2+
import { useEffect, useRef, useState } from 'preact/hooks';
3+
import { useTelemetry } from '../types.js';
4+
import { useCustomizer } from '../customizer/Customizer.js';
5+
import { Telemetry } from './telemetry.js';
6+
7+
export function DebugCustomized({ index }) {
8+
const [isOpen, setOpen] = useState(false);
9+
const telemetry = useTelemetry();
10+
useCustomizer({
11+
title: '🐞 Debug',
12+
id: 'debug',
13+
icon: 'shield',
14+
visibility: isOpen ? 'visible' : 'hidden',
15+
16+
toggle: (_id) => setOpen((prev) => !prev),
17+
index,
18+
});
19+
return (
20+
<div>
21+
<Debug telemetry={telemetry} isOpen={isOpen} />
22+
</div>
23+
);
24+
}
25+
26+
/**
27+
* @param {object} props
28+
* @param {import("./telemetry.js").Telemetry} props.telemetry
29+
* @param {boolean} props.isOpen
30+
*/
31+
export function Debug({ telemetry, isOpen }) {
32+
/** @type {import("preact").Ref<HTMLTextAreaElement>} */
33+
const textRef = useRef(null);
34+
useEvents(textRef, telemetry);
35+
return (
36+
<div hidden={!isOpen}>
37+
<textarea style={{ width: '100%' }} rows={20} ref={textRef}></textarea>
38+
</div>
39+
);
40+
}
41+
42+
/**
43+
* @param {import("preact").RefObject<HTMLTextAreaElement>} ref
44+
* @param {import("./telemetry.js").Telemetry} telemetry
45+
*/
46+
function useEvents(ref, telemetry) {
47+
useEffect(() => {
48+
if (!ref.current) return;
49+
const elem = ref.current;
50+
function handle(/** @type {CustomEvent<any>} */ { detail }) {
51+
elem.value += JSON.stringify(detail, null, 2) + '\n\n';
52+
}
53+
for (const beforeElement of telemetry.eventStore) {
54+
elem.value += JSON.stringify(beforeElement, null, 2) + '\n\n';
55+
}
56+
telemetry.eventStore = [];
57+
telemetry.storeEnabled = false;
58+
telemetry.eventTarget.addEventListener(Telemetry.EVENT_BROADCAST, handle);
59+
return () => {
60+
telemetry.eventTarget.removeEventListener(Telemetry.EVENT_BROADCAST, handle);
61+
telemetry.storeEnabled = true;
62+
};
63+
}, [ref, telemetry]);
64+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/**
2+
* @import { Messaging } from "@duckduckgo/messaging"
3+
*/
4+
export class Telemetry {
5+
static EVENT_REQUEST = 'TELEMETRY_EVENT_REQUEST';
6+
static EVENT_RESPONSE = 'TELEMETRY_EVENT_RESPONSE';
7+
static EVENT_SUBSCRIPTION = 'TELEMETRY_EVENT_SUBSCRIPTION';
8+
static EVENT_SUBSCRIPTION_DATA = 'TELEMETRY_EVENT_SUBSCRIPTION_DATA';
9+
static EVENT_NOTIFICATION = 'TELEMETRY_EVENT_NOTIFICATION';
10+
static EVENT_BROADCAST = 'TELEMETRY_*';
11+
12+
eventTarget = new EventTarget();
13+
/** @type {any[]} */
14+
eventStore = [];
15+
storeEnabled = true;
16+
17+
/**
18+
* @param now
19+
*/
20+
constructor(now = Date.now()) {
21+
this.now = now;
22+
performance.mark('ddg-telemetry-init');
23+
this._setupMessagingMarkers();
24+
}
25+
26+
_setupMessagingMarkers() {
27+
this.eventTarget.addEventListener(Telemetry.EVENT_REQUEST, (/** @type {CustomEvent<any>} */ { detail }) => {
28+
const named = `ddg request ${detail.method} ${detail.timestamp}`;
29+
performance.mark(named);
30+
this.broadcast(detail);
31+
});
32+
this.eventTarget.addEventListener(Telemetry.EVENT_RESPONSE, (/** @type {CustomEvent<any>} */ { detail }) => {
33+
const reqNamed = `ddg request ${detail.method} ${detail.timestamp}`;
34+
const resNamed = `ddg response ${detail.method} ${detail.timestamp}`;
35+
performance.mark(resNamed);
36+
performance.measure(reqNamed, reqNamed, resNamed);
37+
this.broadcast(detail);
38+
});
39+
this.eventTarget.addEventListener(Telemetry.EVENT_SUBSCRIPTION, (/** @type {CustomEvent<any>} */ { detail }) => {
40+
const named = `ddg subscription ${detail.method} ${detail.timestamp}`;
41+
performance.mark(named);
42+
this.broadcast(detail);
43+
});
44+
this.eventTarget.addEventListener(Telemetry.EVENT_SUBSCRIPTION_DATA, (/** @type {CustomEvent<any>} */ { detail }) => {
45+
const named = `ddg subscription data ${detail.method} ${detail.timestamp}`;
46+
performance.mark(named);
47+
this.broadcast(detail);
48+
});
49+
this.eventTarget.addEventListener(Telemetry.EVENT_NOTIFICATION, (/** @type {CustomEvent<any>} */ { detail }) => {
50+
const named = `ddg notification ${detail.method} ${detail.timestamp}`;
51+
performance.mark(named);
52+
this.broadcast(detail);
53+
});
54+
}
55+
56+
broadcast(payload) {
57+
if (this.eventStore.length >= 50) {
58+
this.eventStore = [];
59+
}
60+
if (this.storeEnabled) {
61+
this.eventStore.push(structuredClone(payload));
62+
}
63+
this.eventTarget.dispatchEvent(new CustomEvent(Telemetry.EVENT_BROADCAST, { detail: payload }));
64+
}
65+
66+
measureFromPageLoad(marker, measure = 'measure__' + Date.now()) {
67+
if (!performance.getEntriesByName(marker).length) {
68+
performance.mark(marker);
69+
performance.measure(measure, 'ddg-telemetry-init', marker);
70+
}
71+
}
72+
}
73+
74+
/**
75+
* @implements Messaging
76+
*/
77+
class MessagingObserver {
78+
/** @type {Map<string, number>} */
79+
observed = new Map();
80+
81+
/**
82+
* @param {import("@duckduckgo/messaging").Messaging} messaging
83+
* @param {EventTarget} eventTarget
84+
*/
85+
constructor(messaging, eventTarget) {
86+
this.messaging = messaging;
87+
this.messagingContext = messaging.messagingContext;
88+
this.transport = messaging.transport;
89+
this.eventTarget = eventTarget;
90+
}
91+
92+
/**
93+
* @param {string} method
94+
* @param {Record<string, any>} params
95+
*/
96+
request(method, params) {
97+
const timestamp = Date.now();
98+
const json = {
99+
kind: 'request',
100+
method,
101+
params,
102+
timestamp,
103+
};
104+
this.record(Telemetry.EVENT_REQUEST, json);
105+
return (
106+
this.messaging
107+
.request(method, params)
108+
// eslint-disable-next-line promise/prefer-await-to-then
109+
.then((x) => {
110+
const resJson = {
111+
kind: 'response',
112+
method,
113+
result: x,
114+
timestamp,
115+
};
116+
this.record(Telemetry.EVENT_RESPONSE, resJson);
117+
return x;
118+
})
119+
);
120+
}
121+
122+
/**
123+
* @param {string} method
124+
* @param {Record<string, any>} params
125+
*/
126+
notify(method, params) {
127+
const json = {
128+
kind: 'notification',
129+
method,
130+
params,
131+
};
132+
this.record(Telemetry.EVENT_NOTIFICATION, json);
133+
return this.messaging.notify(method, params);
134+
}
135+
136+
/**
137+
* @param method
138+
* @param callback
139+
* @return {function(): void}
140+
*/
141+
subscribe(method, callback) {
142+
const timestamp = Date.now();
143+
const json = {
144+
kind: 'subscription',
145+
method,
146+
timestamp,
147+
};
148+
149+
this.record(Telemetry.EVENT_SUBSCRIPTION, json);
150+
return this.messaging.subscribe(method, (params) => {
151+
const json = {
152+
kind: 'subscription data',
153+
method,
154+
timestamp,
155+
params,
156+
};
157+
this.record(Telemetry.EVENT_SUBSCRIPTION_DATA, json);
158+
callback(params);
159+
});
160+
}
161+
162+
/**
163+
* @param {string} name
164+
* @param {Record<string, any>} detail
165+
*/
166+
record(name, detail) {
167+
this.eventTarget.dispatchEvent(new CustomEvent(name, { detail }));
168+
}
169+
}
170+
171+
/**
172+
* @param {Messaging} messaging
173+
* @return {{telemetry: Telemetry, messaging: MessagingObserver}}
174+
*/
175+
export function install(messaging) {
176+
const telemetry = new Telemetry();
177+
const observedMessaging = new MessagingObserver(messaging, telemetry.eventTarget);
178+
return { telemetry, messaging: observedMessaging };
179+
}

special-pages/pages/new-tab/app/types.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export function useTypedTranslation() {
2020

2121
export const MessagingContext = createContext(/** @type {import("../src/js/index.js").NewTabPage} */ ({}));
2222
export const useMessaging = () => useContext(MessagingContext);
23+
export const TelemetryContext = createContext(
24+
/** @type {import("./telemetry/telemetry.js").Telemetry} */ ({
25+
measureFromPageLoad: () => {},
26+
}),
27+
);
28+
export const useTelemetry = () => useContext(TelemetryContext);
2329

2430
export const InitialSetupContext = createContext(/** @type {InitialSetupResponse} */ ({}));
2531
export const useInitialSetupData = () => useContext(InitialSetupContext);

special-pages/pages/new-tab/app/widget-list/WidgetList.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { WidgetConfigContext, WidgetVisibilityProvider } from './widget-config.p
33
import { useContext } from 'preact/hooks';
44
import { Stack } from '../../../onboarding/app/components/Stack.js';
55
import { Customizer, CustomizerMenuPositionedFixed } from '../customizer/Customizer.js';
6+
import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';
7+
import { DebugCustomized } from '../telemetry/Debug.js';
68

79
/**
810
* @param {string} id
@@ -36,6 +38,7 @@ export async function widgetEntryPoint(id) {
3638

3739
export function WidgetList() {
3840
const { widgets, widgetConfigItems, entryPoints } = useContext(WidgetConfigContext);
41+
const { env } = useEnv();
3942

4043
return (
4144
<Stack gap={'var(--sp-8)'}>
@@ -53,6 +56,7 @@ export function WidgetList() {
5356
</Fragment>
5457
);
5558
})}
59+
{env === 'development' && <DebugCustomized index={widgets.length} />}
5660
<CustomizerMenuPositionedFixed>
5761
<Customizer />
5862
</CustomizerMenuPositionedFixed>

special-pages/pages/new-tab/src/js/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createTypedMessages } from '@duckduckgo/messaging';
99
import { createSpecialPageMessaging } from '../../../../shared/create-special-page-messaging';
1010
import { Environment } from '../../../../shared/environment.js';
1111
import { mockTransport } from './mock-transport.js';
12+
import { install } from '../../app/telemetry/telemetry.js';
1213

1314
export class NewTabPage {
1415
/**
@@ -57,7 +58,7 @@ export class NewTabPage {
5758

5859
const baseEnvironment = new Environment().withInjectName(import.meta.injectName).withEnv(import.meta.env);
5960

60-
const messaging = createSpecialPageMessaging({
61+
const rawMessaging = createSpecialPageMessaging({
6162
injectName: import.meta.injectName,
6263
env: import.meta.env,
6364
pageName: 'newTabPage',
@@ -71,9 +72,10 @@ const messaging = createSpecialPageMessaging({
7172
},
7273
});
7374

75+
const { messaging, telemetry } = install(rawMessaging);
7476
const newTabMessaging = new NewTabPage(messaging, import.meta.injectName);
7577

76-
init(newTabMessaging, baseEnvironment).catch((e) => {
78+
init(newTabMessaging, telemetry, baseEnvironment).catch((e) => {
7779
console.error(e);
7880
const msg = typeof e?.message === 'string' ? e.message : 'unknown init error';
7981
newTabMessaging.reportInitException(msg);

0 commit comments

Comments
 (0)