Purpose: A complete measurement specification for tracking engagement and interaction patterns across the site, extending the existing src/services/analytics.js layer. This is the plan only — no code changes. It operationalizes the funnels in the Combined Growth Plan §3 and the per-feature metrics in the Per-Feature Growth Plans.
Current state (from code review): a solid baseline already exists — signup_start/complete + login_success/failed (wired in Auth forms), calculator_use (wired in ~12 calculators), heatmap_view / heatmap_interaction / heatmap_directory_view, stock_page_load (data-source diagnostics), feature_view, newsletter_signup, blog_view / blog_cta_click, dashboard_view, document_upload_start/complete, user_first_visit / user_return. What's missing is (a) the interaction layer — what users do within a page, (b) prompt/source attribution for signup, and (c) GA4 property configuration (key events, custom dimensions, funnels) so any of it becomes reportable.
- Naming:
snake_case, object_action order (heatmap_tile_click, chart_range_change). ≤40 chars. Never collide with GA4 reserved/automatic events (click, scroll, page_view, session_start, first_visit).
- Parameters:
snake_case, ≤40-char names, ≤100-char values, ≤25 params per event. Reuse the same param name across events (source, symbol, index_name, sector) — custom-dimension slots are shared, and consistency is what makes cross-event analysis work.
- One event per user intent. A tile click fires
heatmap_tile_click only — not also a generic heatmap_interaction. The existing generic heatmap_interaction(action, label) becomes a legacy alias to be retired once the specific events land.
- No PII in any param (no emails, names, portfolio values). Portfolio amounts are bucketed (
portfolio_size_bucket: "1-5" | "6-15" | "16+" holdings).
- Fire on commitment, not render. Interaction events fire on user action; view events fire once per page per session where feasible (guard with a ref/flag — the current inflated key events came from per-render firing).
- Every conversion-adjacent event carries
source so funnels can attribute which surface produced the signup (source: heatmap_tile | heatmap_nudge | save_view | watchlist | calculator_save | portfolio_upload | dividend_alert | ipo_alert | blog_cta | newsletter | nav).
- All events go through the existing consent-aware
trackEvent() wrapper in analytics.js — no direct gtag calls in components.
From the Combined Growth Plan §3.4 — restated as the tracking plan's step zero:
| # |
Fix |
Why |
| P1 |
Demote the pageview-level key event; promote only the conversions in §6 |
"Key events ≈ views" today; conversion reports are meaningless |
| P2 |
Move GA pageview out of SEO.jsx into a single route-change hook that sets page_title after Helmet resolves |
Kills the title fragmentation ("NIFTY_PHARMA", "(not set)", 3 homepage variants) polluting the Pages report |
| P3 |
Deduplicate view events (once per page per session) |
Current feature_view/heatmap_view fire per mount, inflating counts |
| P4 |
Add a page_not_found event with path + referrer params |
36 unexplained 404 views/month; automatic 404 pages are invisible in GA4 |
| Event |
Trigger |
Params |
Notes |
nav_click |
Header/sidebar nav item clicked |
item, location (header|sidebar|footer|mobile_menu) |
Replaces the ad-hoc AppSideBar ReactGA events; reveals which features users actually reach for |
search_open |
Stock search focused/opened |
location |
|
search_query |
Query submitted / debounced ≥3 chars |
query_length, results_count |
Not the query text (avoids PII/noise); pair with search_result_click |
search_result_click |
Result selected |
symbol, result_rank |
result_rank shows whether search ranking works |
search_abandoned |
Search closed with results shown, none clicked |
results_count |
Discovery-failure signal |
theme_toggle |
Dark/light switched |
to_theme |
Cheap UX signal |
outbound_click |
Link leaving wiseinvest.in |
link_domain |
Enable Enhanced Measurement version if sufficient |
share_click |
Any share button/native share |
content_type (heatmap|stock|calculator|blog), method (whatsapp|twitter|copy_link|native), item_id |
The WhatsApp-forward loop is a growth channel — measure it from day one |
page_not_found |
404 component mounts |
path, referrer |
P4 above |
error_boundary_shown |
React error boundary catches |
component, route |
Silent breakage currently invisible |
session_depth_ping |
Once per session at 3rd unique page |
pages_viewed |
Marks "explorer" sessions for audiences |
scroll_depth |
90% scroll on content pages (blog, stock, calculator landing) |
percent: 90, content_type |
Use GA4 Enhanced Measurement scroll where possible; custom only on SPA route-change pages where EM misses |
| Event |
Trigger |
Params |
Notes |
heatmap_view |
Heatmap rendered (once/page/session) |
index_name, sector, group, view_type (index|sector|group) |
Exists — add dedupe + params |
heatmap_tile_click |
Stock tile clicked → stock page |
symbol, index_name, tile_change_pct_bucket (gain_big|gain|flat|loss|loss_big) |
Do users click winners or losers? Informs newsletter/content angle |
heatmap_tile_watch |
Star/watch on a tile (F1, new UI) |
symbol, index_name, auth_state |
Key funnel entry |
heatmap_index_switch |
Index/sector selector changed |
from_index, to_index |
Which indices get compared — informs which landing pages to build next |
heatmap_timeframe_change |
1D/1W/1M etc. toggled |
timeframe, index_name |
|
heatmap_metric_change |
Color/size metric changed (% change vs mkt-cap etc.) |
metric |
|
heatmap_fullscreen |
Fullscreen toggled |
index_name |
Power-user marker |
heatmap_save_view_click |
"Save this view" (F1, new UI) |
index_name, auth_state |
Fires signup_prompt_shown when anonymous |
heatmap_snapshot_share |
Share/download of heatmap image |
index_name, method |
Alias of share_click with content_type:"heatmap" — pick one, recommend the alias |
heatmap_repeat_visitor_nudge |
3rd-session banner shown/clicked/dismissed |
action (shown|clicked|dismissed) |
Measures the F1 nudge honestly, including annoyance rate |
Deliberately not tracked: tile hover (noise, mobile-meaningless), pan/zoom micro-moves. If tooltip engagement matters later, sample it (1 in 10 sessions) behind a flag.
| Event |
Trigger |
Params |
Notes |
stock_view |
Page viewed (once/page/session) |
symbol, sector, arrived_from (heatmap|search|seo|internal|direct — derive from referrer/state) |
Complements the existing stock_page_load diagnostics (keep that for data health) |
chart_range_change |
1D/1W/1M/1Y/5Y toggled |
symbol, range |
Core engagement pattern; long-range users are researchers → watchlist prompt candidates |
stock_section_view |
Section scrolled into view (financials, analyst, dividends, about) |
symbol, section |
IntersectionObserver, once per section per view. Tells us which sections earn the page's 297s engagement |
watchlist_toggle |
Star clicked |
symbol, action (add|remove), auth_state, source |
watchlist_add (first ever add) is a key event |
stock_index_tag_click |
Index/heatmap tag chip clicked |
symbol, target_index |
Measures stock→heatmap circulation |
stock_peer_click |
Peer/related-stock link clicked |
from_symbol, to_symbol |
Internal-linking effectiveness |
dividend_history_view |
Dividend section expanded/viewed |
symbol |
Bridges to Dividend Calendar plays |
| Event |
Trigger |
Params |
Notes |
calculator_use |
First input change (exists) |
calculator |
Keep; ensure once/session dedupe |
calculator_complete |
Result computed with all required inputs |
calculator, inputs_changed_count |
The real engagement signal — calculator_use alone can be a single keystroke |
calculator_adjust |
Input changed after first result |
calculator |
Scenario-play behavior = high intent |
calculator_save_click |
"Save/email this plan" (F2, new UI) |
calculator, auth_state |
Fires signup_prompt_shown{source:"calculator_save"} when anonymous |
calculator_saved |
Plan persisted to account |
calculator |
Key event |
calculator_shared |
PDF/WhatsApp share of results |
calculator, method |
Use share_click alias |
| Event |
Trigger |
Params |
dividend_calendar_view |
Calendar viewed |
month_offset (0 = current) |
dividend_row_expand |
A dividend row/stock expanded |
symbol |
dividend_alert_click |
"Alert me before ex-date" clicked |
symbol, auth_state |
dividend_alert_set |
Alert confirmed (email/account) |
symbol — key event |
ipo_view |
IPO detail page viewed |
ipo_name, ipo_stage (upcoming|open|closed|listed) |
ipo_gmp_check |
GMP section/tab viewed |
ipo_name |
ipo_alert_set |
"Notify me" confirmed |
ipo_name, alert_type (open|allotment|listing) — key event |
| Event |
Trigger |
Params |
Notes |
blog_view |
Exists |
slug, title |
Keep |
blog_read_complete |
90% scroll + ≥45s on page |
slug |
Distinguishes readers from bouncers; feeds a "readers" audience |
blog_cta_click |
Exists |
slug, cta_type |
Keep — wire destinations per F5 |
news_item_click |
News item opened |
category |
|
news_to_market_click |
"See the sector/stock" link from news |
target_type, target_id |
The news feature's one job (per the feature plan's News section) |
economy_chart_interact |
India-economy chart filtered/toggled |
dataset, action |
|
economy_embed_copy |
Embed code copied |
dataset |
Backlink-loop measurement |
| Event |
Trigger |
Params |
Notes |
signup_prompt_shown |
Any auth gate/modal/nudge rendered |
source (§1.6 list), prompt_type (modal|banner|inline) |
The missing link — today we can't tell how many gates users ever see |
signup_prompt_dismissed |
Gate closed without proceeding |
source |
Annoyance/objection rate per surface |
signup_start / signup_complete |
Exist |
add source (persist from prompt via sessionStorage), method (email|google) |
signup_complete = key event |
signup_error |
Cognito error surfaced |
step (form|verification), error_code |
The 6-second Create Account bounce needs a cause breakdown |
verification_complete |
Email verified |
method |
|
login_success / login_failed |
Exist |
keep; add method |
|
session_restored |
Auto-login from persisted session |
— |
Measures the session-persistence fix (154 login views / 13 users) |
onboarding_step |
Each wealth-onboarding step reached |
step_name, step_number |
The flow exists (src/components/Onboarding/*) with zero step visibility |
portfolio_upload_start / portfolio_upload_complete |
F3 upload flow |
method (csv|email_import), holdings_bucket |
_complete = key event |
portfolio_heatmap_rendered |
Personal heatmap shown |
holdings_bucket |
The wow moment — timestamp gap from upload_start = time-to-value |
activation_complete |
3 watchlist adds OR portfolio uploaded OR newsletter-linked return, within 7 days |
via |
Key event; computed client-side on the qualifying action |
newsletter_signup |
Exists |
ensure source on every call site |
Key event |
newsletter_link_visit |
Landing with utm_source=newsletter |
campaign |
Session-scoped, from UTM |
| Property |
Values |
Purpose |
auth_state |
anonymous | registered | verified |
Segment every report by login state |
visitor_tier |
new | returning | habitual (≥4 visits/30d, from the existing localStorage counter) |
The F1 nudge targets habitual; audiences build on this |
first_touch_source |
seo | newsletter | social | direct (first session, persisted) |
Long-window attribution GA4's last-click hides |
has_watchlist / has_portfolio |
boolean |
Activation segmentation |
Event-scoped (limit 50; this uses 14): source, symbol, index_name, sector, calculator, slug, method, action, prompt_type, error_code, step_name, ipo_name, content_type, result_rank.
User-scoped (limit 25; uses 4): the §4 properties.
Register before launch — GA4 does not backfill dimension data.
signup_complete · newsletter_signup · watchlist_add (first) · calculator_saved · portfolio_upload_complete · dividend_alert_set · ipo_alert_set · activation_complete. Nothing else. Demote whatever pageview-level event is currently marked (P1).
- Master:
first_visit → heatmap_view → signup_prompt_shown → signup_start → signup_complete → activation_complete (open funnel, 30-day window).
- F1 Heatmap:
heatmap_view → heatmap_tile_click → stock_view → watchlist_toggle(add) → signup_complete — segmented by visitor_tier.
- F2 Calculator:
calculator_use → calculator_complete → calculator_save_click → signup_complete.
- F3 Portfolio:
portfolio_upload_start → portfolio_upload_complete → portfolio_heatmap_rendered → signup_complete.
- Signup diagnosis:
signup_prompt_shown → signup_start → signup_error?/signup_complete → verification_complete, broken down by source and method — this is the report that explains the 6-second bounce.
- Search quality:
search_open → search_query → search_result_click | search_abandoned.
Plus audiences: Habitual heatmap users (≥3 heatmap_view sessions, auth_state=anonymous) — the F1 nudge target and a remarketing seed; Calculator intenders (calculator_complete, no account); Readers (blog_read_complete).
| File |
Work |
src/services/analytics.js |
Add ~25 typed helpers above; add setUserProperties(); add sessionStorage source persistence helper for the prompt→signup chain |
src/App.jsx (new usePageTracking hook) |
P2 pageview relocation; session_depth_ping; UTM capture → first_touch_source |
src/pages/Heatmap.jsx |
Tile click/watch, index switch, timeframe, metric, fullscreen, save-view, nudge events |
src/pages/stocks/StockPage.jsx |
stock_view w/ arrived_from, chart_range_change, stock_section_view (IntersectionObserver), watchlist events, tag/peer clicks |
src/pages/calculators/* |
calculator_complete / calculator_adjust / save-click (shared hook useCalculatorTracking(type) to avoid 12× duplication) |
src/components/Auth/* |
signup_error, source+method params, verification_complete, session_restored |
src/components/Onboarding/* |
onboarding_step at each view transition |
src/components/newsletter/SubscribeBand.jsx |
Ensure source param at every placement |
| 404 page component |
page_not_found |
src/components/AppSideBar.jsx |
Replace ad-hoc ReactGA events with nav_click |
- Wave 1 (with growth-plan week 1): P1–P4 fixes, custom-dimension registration,
signup_prompt_shown/dismissed, signup_error, nav_click, page_not_found, user properties.
- Wave 2 (with F1 build): all heatmap + stock interaction events, watchlist events.
- Wave 3 (with F2–F4): calculator, dividend/IPO alert, newsletter-source, portfolio/onboarding events.
- QA: every new event verified in GA4 DebugView (
?debug_mode=1) before merge; a docs/gtm/event-dictionary table kept in sync (this file is the source of truth); weekly check of Realtime → Events for name typos (GA4 silently accepts anything).
- Consent: all events already no-op via
ConsentManager — verify the new user-properties call respects the same gate.
- Budget guard: stay well under GA4's 500-distinct-event-names limit (this plan adds ~35, total ≈50) and never fire >1 event per user action.
- Which surface earns signups (
signup_prompt_shown.source → signup_complete.source) — directly prices the F1–F6 funnels against each other.
- Why signup fails (
signup_error.error_code + prompt-dismiss rates) — turns the 6-second Create Account bounce from a mystery into a bug list.
- What makes heatmaps sticky (tile-click polarity, index switches, timeframes) — feeds newsletter content and which landing pages to build next.
- Whether internal circulation works (heatmap→stock→heatmap via
arrived_from, tag clicks, peer clicks) — the SEO plan's internal-linking thesis, verified behaviorally.
- Time-to-value for F3 (
upload_start → heatmap_rendered gap) — the number to optimize in the highest-intent funnel.
- True content performance (
blog_read_complete vs views) — ranks posts by readers, not clicks, for the 2-posts/month editorial slots.