From e3899d4755f9bbed202e1dd34f66ace18e2e4f97 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Tue, 23 Jun 2026 10:18:02 +0200 Subject: [PATCH] feat(ui): record the SDK load outcome in the launch diagnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The diagnostic showed only sdk: yes/no (window.Telegram presence), not why the SDK was absent. Capture how the dynamic telegram-web-app.js load resolved — present / loaded / no-webapp / error / timeout — and surface it as "sdk-load: ". error/timeout pinpoint a blocked or hanging telegram.org (the prime suspect for an empty launch); no-webapp a loaded-but-broken script. loadTelegramSDK records the outcome (telegramSdkOutcome); collectTelegramDiag carries it into the screen. Unit tests cover each outcome; the blocked-script e2e now asserts sdk-load: error. --- ui/e2e/telegram.spec.ts | 3 +- ui/src/lib/telegram.test.ts | 37 ++++++++++++++++++-- ui/src/lib/telegram.ts | 41 +++++++++++++++++++---- ui/src/screens/TelegramLaunchError.svelte | 1 + 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/ui/e2e/telegram.spec.ts b/ui/e2e/telegram.spec.ts index a4a22e6..073461d 100644 --- a/ui/e2e/telegram.spec.ts +++ b/ui/e2e/telegram.spec.ts @@ -131,5 +131,6 @@ test('a blocked telegram-web-app.js does not hang the diagnostic screen', async await page.goto('/telegram/'); await expect(page.getByRole('button', { name: 'Share' })).toBeVisible(); - await expect(page.getByText('sdk: no')).toBeVisible(); + // The diagnostic names the load outcome: a failed fetch reads as sdk-load: error. + await expect(page.getByText('sdk-load: error')).toBeVisible(); }); diff --git a/ui/src/lib/telegram.test.ts b/ui/src/lib/telegram.test.ts index cb71a30..22a5108 100644 --- a/ui/src/lib/telegram.test.ts +++ b/ui/src/lib/telegram.test.ts @@ -3,6 +3,7 @@ import { collectTelegramDiag, insideTelegram, loadTelegramSDK, + telegramSdkOutcome, routeExternalLinkInTelegram, telegramClosingConfirmation, telegramLaunch, @@ -205,13 +206,41 @@ describe('loadTelegramSDK', () => { vi.useRealTimers(); }); - it('resolves true immediately when the SDK is already present', async () => { + it('resolves true and records "present" when the SDK is already there', async () => { vi.stubGlobal('window', { Telegram: { WebApp: { initData: '' } } }); vi.stubGlobal('document', { createElement: vi.fn(), head: { appendChild: vi.fn() } }); await expect(loadTelegramSDK(10000)).resolves.toBe(true); + expect(telegramSdkOutcome()).toBe('present'); }); - it('resolves false when the script errors (telegram.org unreachable)', async () => { + it('records "loaded" when the script defines the WebApp', async () => { + const script: Record = {}; + vi.stubGlobal('window', {}); + vi.stubGlobal('document', { + createElement: () => script, + head: { + appendChild: () => { + (window as unknown as { Telegram: unknown }).Telegram = { WebApp: { initData: '' } }; + (script.onload as () => void)(); + }, + }, + }); + await expect(loadTelegramSDK(10000)).resolves.toBe(true); + expect(telegramSdkOutcome()).toBe('loaded'); + }); + + it('records "no-webapp" when the script loads but defines nothing', async () => { + const script: Record = {}; + vi.stubGlobal('window', {}); + vi.stubGlobal('document', { + createElement: () => script, + head: { appendChild: () => (script.onload as () => void)() }, + }); + await expect(loadTelegramSDK(10000)).resolves.toBe(false); + expect(telegramSdkOutcome()).toBe('no-webapp'); + }); + + it('records "error" when the script fails to load (telegram.org unreachable)', async () => { const script: Record = {}; vi.stubGlobal('window', {}); vi.stubGlobal('document', { @@ -219,14 +248,16 @@ describe('loadTelegramSDK', () => { head: { appendChild: () => (script.onerror as () => void)() }, }); await expect(loadTelegramSDK(10000)).resolves.toBe(false); + expect(telegramSdkOutcome()).toBe('error'); }); - it('resolves false when the script hangs past the timeout', async () => { + it('records "timeout" when the script neither loads nor fails in time', async () => { vi.useFakeTimers(); vi.stubGlobal('window', {}); vi.stubGlobal('document', { createElement: () => ({}), head: { appendChild: vi.fn() } }); const p = loadTelegramSDK(10000); await vi.advanceTimersByTimeAsync(10000); await expect(p).resolves.toBe(false); + expect(telegramSdkOutcome()).toBe('timeout'); }); }); diff --git a/ui/src/lib/telegram.ts b/ui/src/lib/telegram.ts index be4ca76..8d529d2 100644 --- a/ui/src/lib/telegram.ts +++ b/ui/src/lib/telegram.ts @@ -60,6 +60,27 @@ export function insideTelegram(): boolean { // safe-area insets, vertical-swipe guard, …) are available. Bump it when Telegram bumps theirs. const sdkScriptSrc = 'https://telegram.org/js/telegram-web-app.js?62'; +/** + * TelegramSdkOutcome records how the dynamic telegram-web-app.js load resolved, surfaced on the + * launch-error screen to tell the failure modes apart — notably 'error' / 'timeout', which mean the + * network could not reach telegram.org (the script blocked or hung): + * + * not-attempted — loadTelegramSDK was never called (a plain web / native entry) + * present — window.Telegram.WebApp was already there (a cached load or native injection) + * loaded — the script loaded and defined window.Telegram.WebApp + * no-webapp — the script loaded (HTTP 200) but did not define window.Telegram.WebApp + * error — the script failed to load (telegram.org unreachable / blocked, a fast failure) + * timeout — the script neither loaded nor failed within the timeout (a blocked, hanging fetch) + */ +export type TelegramSdkOutcome = 'not-attempted' | 'present' | 'loaded' | 'no-webapp' | 'error' | 'timeout'; + +let sdkLoadOutcome: TelegramSdkOutcome = 'not-attempted'; + +/** telegramSdkOutcome returns how the last loadTelegramSDK attempt resolved (see TelegramSdkOutcome). */ +export function telegramSdkOutcome(): TelegramSdkOutcome { + return sdkLoadOutcome; +} + /** * loadTelegramSDK injects the official telegram-web-app.js and resolves true once * window.Telegram.WebApp is available, or false if the script errors or does not load within @@ -72,25 +93,29 @@ const sdkScriptSrc = 'https://telegram.org/js/telegram-web-app.js?62'; */ export function loadTelegramSDK(timeoutMs: number): Promise { if (typeof document === 'undefined') return Promise.resolve(false); - if (webApp()) return Promise.resolve(true); + if (webApp()) { + sdkLoadOutcome = 'present'; + return Promise.resolve(true); + } return new Promise((resolve) => { let done = false; - const finish = (ok: boolean): void => { + const finish = (outcome: TelegramSdkOutcome): void => { if (done) return; done = true; - resolve(ok); + sdkLoadOutcome = outcome; + resolve(outcome === 'loaded'); }; - const timer = setTimeout(() => finish(!!webApp()), timeoutMs); + const timer = setTimeout(() => finish(webApp() ? 'loaded' : 'timeout'), timeoutMs); const s = document.createElement('script'); s.src = sdkScriptSrc; s.async = true; s.onload = () => { clearTimeout(timer); - finish(!!webApp()); + finish(webApp() ? 'loaded' : 'no-webapp'); }; s.onerror = () => { clearTimeout(timer); - finish(false); + finish('error'); }; document.head.appendChild(s); }); @@ -413,6 +438,9 @@ export interface TelegramDiag { hasSDK: boolean; /** Whether window.Telegram.WebApp is present. */ hasWebApp: boolean; + /** How the dynamic telegram-web-app.js load resolved (see TelegramSdkOutcome) — 'error' / + * 'timeout' mean telegram.org was unreachable, the prime suspect for an empty launch. */ + sdkLoad: TelegramSdkOutcome; /** Telegram's own platform string (ios | android | android_x | tdesktop | web | …), or ''. */ platform: string; /** The Bot API version the client reports, or ''. */ @@ -454,6 +482,7 @@ export function collectTelegramDiag(): TelegramDiag { return { hasSDK: sdk, hasWebApp: !!w, + sdkLoad: sdkLoadOutcome, platform: w?.platform ?? '', version: w?.version ?? '', initDataLen: initData.length, diff --git a/ui/src/screens/TelegramLaunchError.svelte b/ui/src/screens/TelegramLaunchError.svelte index 979e2e0..ecf9c58 100644 --- a/ui/src/screens/TelegramLaunchError.svelte +++ b/ui/src/screens/TelegramLaunchError.svelte @@ -17,6 +17,7 @@ const report = $derived( diag ? [ + `sdk-load: ${diag.sdkLoad}`, `sdk: ${diag.hasSDK ? 'yes' : 'no'} webapp: ${diag.hasWebApp ? 'yes' : 'no'}`, `tg-platform: ${diag.platform || '—'} tg-version: ${diag.version || '—'}`, `initData: ${diag.initDataLen > 0 ? diag.initDataLen : 'empty'} tgdata-in-url: ${diag.hashHadTgData ? 'yes' : 'no'}`,