Skip to content

Commit 540adfd

Browse files
authored
Merge pull request #235 from jaydestro/v2-reorg
wrapping up changes for 2026 until CSC ends
2 parents 8d235d4 + bf63575 commit 540adfd

6 files changed

Lines changed: 271 additions & 56 deletions

File tree

client/docusaurus.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ const config: Config = {
2424
tagline: 'Infinite Scale, Instant Impact!',
2525
favicon: 'img/favicon.ico',
2626

27-
clientModules: ['./src/clientModules/confMobileHashActive.ts'],
27+
clientModules: [
28+
'./src/clientModules/confMobileHashActive.ts',
29+
'./src/clientModules/clientErrorLogger.ts',
30+
],
2831

2932
url: 'https://developer.azurecosmosdb.com',
3033
baseUrl: '/',
Lines changed: 255 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,274 @@
1+
/**
2+
* Client-side error + performance logger.
3+
*
4+
* Captures runtime errors, unhandled promise rejections, failed resource
5+
* loads, slow long-tasks, and a few page-load timings. Persists the most
6+
* recent events to localStorage (capped) so you can pull them back after a
7+
* page crash or slowdown.
8+
*
9+
* Logs are NOT shipped anywhere — they live only in the browser. To inspect
10+
* them:
11+
*
12+
* 1. Open DevTools console (errors print there in real time, prefixed
13+
* `[client-error]` / `[client-perf]`).
14+
* 2. Run `window.__getClientLog()` — returns the current buffer as an
15+
* array.
16+
* 3. Run `window.__downloadClientLog()` — downloads the buffer as a
17+
* JSON file you can attach to a bug report. Or visit any URL on the
18+
* site with `?downloadClientLog` to trigger the download
19+
* automatically.
20+
* 4. Run `window.__clearClientLog()` to reset the buffer.
21+
*
22+
* Nothing here is committed to git: logs only live in localStorage / Blob
23+
* downloads, and the *.log gitignore at repo root blocks any local files
24+
* you save from the download dialog.
25+
*/
26+
27+
interface LogEntry {
28+
ts: string;
29+
kind:
30+
| "error"
31+
| "unhandledrejection"
32+
| "resource-error"
33+
| "long-task"
34+
| "navigation"
35+
| "console-error"
36+
| "console-warn";
37+
href: string;
38+
details: Record<string, unknown>;
39+
}
40+
41+
const LOG_KEY = "__cosmosconf_client_log__";
42+
const MAX_ENTRIES = 200;
43+
const LONG_TASK_THRESHOLD_MS = 200;
44+
45+
function safeStringify(value: unknown): string {
46+
if (value instanceof Error) {
47+
return `${value.name}: ${value.message}\n${value.stack ?? ""}`;
48+
}
49+
if (typeof value === "string") return value;
50+
try {
51+
return JSON.stringify(value);
52+
} catch {
53+
return String(value);
54+
}
55+
}
56+
157
function runWhenBrowser(fn: () => void): void {
258
if (typeof window === "undefined" || typeof document === "undefined") return;
359
fn();
460
}
561

662
runWhenBrowser(() => {
7-
const logPrefix = "[client-error]";
63+
const w = window as unknown as {
64+
__getClientLog?: () => LogEntry[];
65+
__clearClientLog?: () => void;
66+
__downloadClientLog?: () => void;
67+
};
868

9-
window.addEventListener("error", (event) => {
10-
const errorEvent = event as ErrorEvent;
11-
const details = {
12-
message: errorEvent.message,
13-
filename: errorEvent.filename,
14-
lineno: errorEvent.lineno,
15-
colno: errorEvent.colno,
16-
stack: errorEvent.error?.stack,
69+
const readBuffer = (): LogEntry[] => {
70+
try {
71+
const raw = window.localStorage.getItem(LOG_KEY);
72+
if (!raw) return [];
73+
const parsed = JSON.parse(raw);
74+
return Array.isArray(parsed) ? (parsed as LogEntry[]) : [];
75+
} catch {
76+
return [];
77+
}
78+
};
79+
80+
const writeBuffer = (entries: LogEntry[]): void => {
81+
try {
82+
window.localStorage.setItem(LOG_KEY, JSON.stringify(entries));
83+
} catch {
84+
// Quota or privacy mode — ignore.
85+
}
86+
};
87+
88+
const push = (entry: Omit<LogEntry, "ts" | "href">): void => {
89+
const full: LogEntry = {
90+
ts: new Date().toISOString(),
1791
href: window.location.href,
18-
userAgent: navigator.userAgent,
92+
...entry,
1993
};
20-
console.error(logPrefix, "window.error", details);
21-
});
94+
const buffer = readBuffer();
95+
buffer.push(full);
96+
if (buffer.length > MAX_ENTRIES) buffer.splice(0, buffer.length - MAX_ENTRIES);
97+
writeBuffer(buffer);
98+
};
2299

100+
// Uncaught runtime errors + failed resource loads (capture phase needed
101+
// for resource errors since they don't bubble).
102+
window.addEventListener(
103+
"error",
104+
(event) => {
105+
const target = event.target as
106+
| (HTMLElement & { src?: string; href?: string })
107+
| null;
108+
const isResourceError =
109+
!!target &&
110+
target !== (window as unknown as EventTarget) &&
111+
(target.tagName === "IMG" ||
112+
target.tagName === "SCRIPT" ||
113+
target.tagName === "LINK" ||
114+
target.tagName === "VIDEO" ||
115+
target.tagName === "AUDIO" ||
116+
target.tagName === "SOURCE");
117+
118+
if (isResourceError) {
119+
const details = {
120+
tag: target!.tagName,
121+
url: target!.src || target!.href,
122+
userAgent: navigator.userAgent,
123+
};
124+
// eslint-disable-next-line no-console
125+
console.error("[client-error]", "resource-error", details);
126+
push({ kind: "resource-error", details });
127+
return;
128+
}
129+
130+
const errorEvent = event as ErrorEvent;
131+
const details = {
132+
message: errorEvent.message,
133+
filename: errorEvent.filename,
134+
lineno: errorEvent.lineno,
135+
colno: errorEvent.colno,
136+
stack: errorEvent.error?.stack,
137+
userAgent: navigator.userAgent,
138+
};
139+
// eslint-disable-next-line no-console
140+
console.error("[client-error]", "window.error", details);
141+
push({ kind: "error", details });
142+
},
143+
true
144+
);
145+
146+
// Unhandled promise rejections.
23147
window.addEventListener("unhandledrejection", (event) => {
24148
const reason = event.reason as Error | string | undefined;
25149
const details = {
26150
reason: typeof reason === "string" ? reason : reason?.message,
27151
stack: typeof reason === "string" ? undefined : reason?.stack,
28-
href: window.location.href,
29152
userAgent: navigator.userAgent,
30153
};
31-
console.error(logPrefix, "unhandledrejection", details);
154+
// eslint-disable-next-line no-console
155+
console.error("[client-error]", "unhandledrejection", details);
156+
push({ kind: "unhandledrejection", details });
32157
});
158+
159+
// Wrap console.error / console.warn so React hydration mismatches and
160+
// Docusaurus runtime warnings get persisted, not just printed.
161+
const origError = console.error.bind(console);
162+
console.error = (...args: unknown[]) => {
163+
try {
164+
const first = args[0];
165+
// Avoid recursion on our own logs.
166+
if (!(typeof first === "string" && first.startsWith("[client-"))) {
167+
push({
168+
kind: "console-error",
169+
details: { args: args.map(safeStringify) },
170+
});
171+
}
172+
} catch {
173+
// ignore
174+
}
175+
origError(...args);
176+
};
177+
178+
const origWarn = console.warn.bind(console);
179+
console.warn = (...args: unknown[]) => {
180+
try {
181+
const first = args[0];
182+
if (!(typeof first === "string" && first.startsWith("[client-"))) {
183+
push({
184+
kind: "console-warn",
185+
details: { args: args.map(safeStringify) },
186+
});
187+
}
188+
} catch {
189+
// ignore
190+
}
191+
origWarn(...args);
192+
};
193+
194+
// Long-task observer — anything that blocks the main thread above the
195+
// threshold is a likely cause of perceived slowness.
196+
if (typeof PerformanceObserver !== "undefined") {
197+
try {
198+
const longTaskObserver = new PerformanceObserver((list) => {
199+
for (const entry of list.getEntries()) {
200+
if (entry.duration < LONG_TASK_THRESHOLD_MS) continue;
201+
const details = {
202+
duration: Math.round(entry.duration),
203+
startTime: Math.round(entry.startTime),
204+
name: entry.name,
205+
entryType: entry.entryType,
206+
};
207+
// eslint-disable-next-line no-console
208+
console.warn("[client-perf]", "long-task", details);
209+
push({ kind: "long-task", details });
210+
}
211+
});
212+
longTaskObserver.observe({ type: "longtask", buffered: true });
213+
} catch {
214+
// longtask not supported in this browser — non-fatal.
215+
}
216+
217+
try {
218+
const navObserver = new PerformanceObserver((list) => {
219+
for (const entry of list.getEntries() as PerformanceNavigationTiming[]) {
220+
const details = {
221+
type: entry.type,
222+
duration: Math.round(entry.duration),
223+
domContentLoaded: Math.round(
224+
entry.domContentLoadedEventEnd - entry.startTime
225+
),
226+
loadEvent: Math.round(entry.loadEventEnd - entry.startTime),
227+
transferSize: entry.transferSize,
228+
};
229+
push({ kind: "navigation", details });
230+
}
231+
});
232+
navObserver.observe({ type: "navigation", buffered: true });
233+
} catch {
234+
// ignore
235+
}
236+
}
237+
238+
// Helpers exposed on window.
239+
w.__getClientLog = () => readBuffer();
240+
w.__clearClientLog = () => writeBuffer([]);
241+
w.__downloadClientLog = () => {
242+
const data = readBuffer();
243+
const blob = new Blob([JSON.stringify(data, null, 2)], {
244+
type: "application/json",
245+
});
246+
const url = URL.createObjectURL(blob);
247+
const a = document.createElement("a");
248+
a.href = url;
249+
a.download = `cosmosconf-client-log-${new Date()
250+
.toISOString()
251+
.replace(/[:.]/g, "-")}.json`;
252+
document.body.appendChild(a);
253+
a.click();
254+
document.body.removeChild(a);
255+
URL.revokeObjectURL(url);
256+
};
257+
258+
// `?downloadClientLog` query param triggers an automatic download.
259+
try {
260+
const params = new URLSearchParams(window.location.search);
261+
if (params.has("downloadClientLog")) {
262+
window.setTimeout(() => w.__downloadClientLog?.(), 1000);
263+
}
264+
} catch {
265+
// ignore
266+
}
267+
268+
// One-line breadcrumb so you know the logger is alive.
269+
// eslint-disable-next-line no-console
270+
console.info(
271+
"[client-perf]",
272+
"logger ready — window.__getClientLog() / __downloadClientLog() / __clearClientLog()"
273+
);
33274
});

client/src/conf/videoRelease.ts

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useEffect, useState } from "react";
21
import agendaData from "../pages/conf/agenda.json";
32

43
// Videos become clickable on 2026-04-28 1:30 PM PDT (PDT = UTC-7 → 20:30 UTC).
@@ -55,43 +54,15 @@ export const getVideoUrlForSpeaker = (slug: string): string | undefined => {
5554
};
5655

5756
/**
58-
* Returns true once the shared release time has passed. The value is `false`
59-
* during SSR/initial render to avoid hydration mismatch, then flips to `true`
60-
* on the client if applicable. Re-checked every minute.
61-
*
62-
* Supports `?releaseNow` or `?streamNow` (case-insensitive, value optional;
63-
* `0`/`false` opts out) to force-release for testing/previewing.
57+
* Post-event: time-based gating is suspended. Both hooks now always return
58+
* `true` so the site is permanently in its post-event state (videos clickable,
59+
* stream section shows the thank-you / recording card, no countdown, no
60+
* "We're live" indicator). The timestamp constants and `hasPreviewParam`
61+
* helper are kept in case time-gating needs to be re-enabled for a future
62+
* event.
6463
*/
65-
export const useVideoReleased = (): boolean => {
66-
const [released, setReleased] = useState(false);
67-
useEffect(() => {
68-
const forceReleased = hasPreviewParam(["releaseNow", "streamNow"]);
69-
const check = () =>
70-
setReleased(forceReleased || Date.now() >= VIDEO_RELEASE_TIMESTAMP);
71-
check();
72-
const interval = window.setInterval(check, 60_000);
73-
return () => window.clearInterval(interval);
74-
}, []);
75-
return released;
76-
};
77-
78-
/**
79-
* Returns true once the live stream release time has passed. SSR-safe.
80-
* Supports `?streamNow` or `?releaseNow` (case-insensitive, value optional;
81-
* `0`/`false` opts out) for preview.
82-
*/
83-
export const useStreamReleased = (): boolean => {
84-
const [released, setReleased] = useState(false);
85-
useEffect(() => {
86-
const forceReleased = hasPreviewParam(["streamNow", "releaseNow"]);
87-
const check = () =>
88-
setReleased(forceReleased || Date.now() >= STREAM_RELEASE_TIMESTAMP);
89-
check();
90-
const interval = window.setInterval(check, 60_000);
91-
return () => window.clearInterval(interval);
92-
}, []);
93-
return released;
94-
};
64+
export const useVideoReleased = (): boolean => true;
65+
export const useStreamReleased = (): boolean => true;
9566

9667
/**
9768
* Convert a youtu.be or youtube.com URL into a youtube.com/embed/<id> URL.

client/src/pages/conf/faqs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[
22
{
33
"question": "Where can I watch the Azure Cosmos DB Conf stream?",
4-
"content": "Watch the live stream right here on <a class='blue' href='#stream'>this page</a> on April 28, 2026, or on the <a class='blue' href='https://youtube.com/live/OdPFriVuKtU' target='_blank' rel='noopener noreferrer'>Microsoft Developer YouTube Channel</a>. All sessions will be available on-demand afterward."
4+
"content": "Azure Cosmos DB Conf 2026 streamed live on April 28, 2026. Every session is now available on demand right here on <a class='blue' href='#stream'>this page</a> and on the <a class='blue' href='https://aka.ms/CosmosConf26Playlist' target='_blank' rel='noopener noreferrer'>Microsoft Developer YouTube Channel</a>."
55
},
66
{
77
"question": "Is there a code of conduct for Azure Cosmos DB Conf 2026?",

client/src/pages/conf/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,8 @@ const ConfPage = () => {
273273
from Microsoft and community experts.
274274
</p>
275275
<p className={styles.introCopySecondary}>
276-
Tune in for our engaging 5-hour live show on <strong>{CONF_DATE_LONG}</strong>, and
277-
explore additional sessions on-demand. This is an event you won&apos;t want to miss!
276+
Our 5-hour live show streamed on <strong>{CONF_DATE_LONG}</strong>. Every session
277+
— keynote, breakouts, and community talks — is now available on demand.
278278
</p>
279279
</div>
280280
</div>

client/src/pages/conf/sections/AgendaSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ const AgendaSection = ({ confYear }: AgendaSectionProps) => {
8787
Event agenda
8888
</h2>
8989
<p className={styles.newsDescription}>
90-
Join us live on April 28, 2026 from 9:00 AM to 2:00 PM PDT. All times shown in Pacific Daylight Time. Session recordings will be available on demand after the event.
90+
Azure Cosmos DB Conf 2026 streamed live on April 28, 2026 from 9:00 AM to 2:00 PM PDT. All times shown in Pacific Daylight Time. Every session is now available on demand.
9191
</p>
9292
</div>
9393

0 commit comments

Comments
 (0)