From 33a155bb820551c690cc048e27ca168c351b67c6 Mon Sep 17 00:00:00 2001 From: Navid Shad Date: Fri, 16 Oct 2026 18:59:35 +0300 Subject: [PATCH] feat(analytics): E-03 track the save-login prompt and login results - Word card: save-login-prompt_viewed fires once per word view when "Login to save this phrase" shows (a new word or a new view resets it). Clicking it sends save-login-prompt_clicked. - OpenLoginWindowMessage carries a source (save_prompt, practice, popup). The background puts it on the login window URL (popup.html#/login?source=...), and LoginView reads it. No source means the toolbar popup. - user_logged-in gains source. New login_failed with source and reason: window_closed (Google consent window closed), google_error (no Google token), server_error (our server failed or sent no token, or the password login was rejected). - The Google flow no longer stores an empty token when the server reply has none; it counts as a failed login. - practice-now_login-clicked is kept. - Tests: tests/save-login-prompt.test.ts, tests/login-analytics.test.ts. Work list: subturtle-doc docs/tech/analytics-fixes-implementation.md Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 + src/background.ts | 7 +- src/common/types/messaging.ts | 12 +- .../modules/practice-config/index.vue | 2 +- .../modules/word-detail/index.vue | 28 ++- src/popup/helper/login-analytics.ts | 36 ++++ src/popup/views/IntroView.vue | 2 +- src/popup/views/LoginView.vue | 37 +++- tests/login-analytics.test.ts | 178 ++++++++++++++++++ tests/save-login-prompt.test.ts | 137 ++++++++++++++ 10 files changed, 433 insertions(+), 8 deletions(-) create mode 100644 src/popup/helper/login-analytics.ts create mode 100644 tests/login-analytics.test.ts create mode 100644 tests/save-login-prompt.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 5cd2e44..861b007 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -446,6 +446,8 @@ tests/ translate-card.test.ts # popup translate input flow login-password.test.ts # popup password form (ENABLE_PASSWORD_AUTH=true) login-password-disabled.test.ts # popup password form hidden when flag is unset + login-analytics.test.ts # user_logged-in source, login_failed source + reason (E-03) + save-login-prompt.test.ts # "Login to save" prompt viewed once per word view, clicked (E-03) language-state.test.ts # round-1 rules: dark gate, own language, save row, plan limit, deep link language-catalog.test.ts # one RTL list, picker names, bundled Vazirmatn own-language.test.ts # translate request with / without a server profile; language store diff --git a/src/background.ts b/src/background.ts index 20a3a2f..6cbf214 100644 --- a/src/background.ts +++ b/src/background.ts @@ -106,8 +106,13 @@ chrome.runtime.onMessage.addListener(function (request, _sender, sendResponse) { const left = screen.width / 2 - width / 2; const top = screen.height / 2 - height / 2; + // The login source rides on the hash route's query (E-03). + const query = request.source + ? `?source=${encodeURIComponent(request.source)}` + : ""; + chrome.windows.create({ - url: chrome.runtime.getURL("popup.html") + "#/login", + url: chrome.runtime.getURL("popup.html") + "#/login" + query, type: "popup", width: width, height: height, diff --git a/src/common/types/messaging.ts b/src/common/types/messaging.ts index c80cd62..8ae1fa1 100644 --- a/src/common/types/messaging.ts +++ b/src/common/types/messaging.ts @@ -74,11 +74,21 @@ export class StoreUserTokenMessage implements BaseMessage { } } +/** + * Where a login started (E-03). The background puts it on the login window's + * URL (`popup.html#/login?source=…`) so LoginView can send it with + * `user_logged-in` and `login_failed`. A login opened from the toolbar popup + * itself has no source on the URL and counts as "popup". + */ +export type LoginSource = "save_prompt" | "practice" | "popup"; + export class OpenLoginWindowMessage implements BaseMessage { type: string; + source?: LoginSource; - constructor() { + constructor(source?: LoginSource) { this.type = MESSAGE_TYPE.OPEN_LOGIN_WINDOW; + if (source) this.source = source; } static is(message: any): message is OpenLoginWindowMessage { diff --git a/src/console-crane/modules/practice-config/index.vue b/src/console-crane/modules/practice-config/index.vue index 395e9b9..18b48ce 100644 --- a/src/console-crane/modules/practice-config/index.vue +++ b/src/console-crane/modules/practice-config/index.vue @@ -324,7 +324,7 @@ function upgrade() { /** Logged out — open the login window; the isLogin watch advances the page. */ function handleLoginRequest() { analytic.track("practice-now_login-clicked"); - sendMessage(new OpenLoginWindowMessage()); + sendMessage(new OpenLoginWindowMessage("practice")); } function start() { diff --git a/src/console-crane/modules/word-detail/index.vue b/src/console-crane/modules/word-detail/index.vue index e4e45a8..d3a849c 100644 --- a/src/console-crane/modules/word-detail/index.vue +++ b/src/console-crane/modules/word-detail/index.vue @@ -249,7 +249,7 @@