Skip to content

Commit 10e28c6

Browse files
allilevinematticbot
authored andcommitted
Write: add anon funnel Tracks events + is_anon on editor open (#50216)
* Write: add anon funnel Tracks events + is_anon on editor open Instruments the two intent steps of the Write On Phase 1 fake-door funnel that were missing between "land" and "signup": * wpcom_write_editor_anon_write_start — fires once per session on the first real keystroke in the anon editor (guards against empty-editor abandons counting as writes). * wpcom_write_editor_anon_publish_click — fires when an anon visitor hits the signup wall at Publish, with word_count, time_to_publish_ms, draft_size_bytes. Both go through the _tkq queue and share the tk_ai cookie, so they stitch to the new user at signup completion (identifyUser via the write-on flow's builtin-auth registration path). Also adds is_anon to the server-side wpcom_write_editor_open event ((int) ! is_user_logged_in() — anon is the only logged-out render of this editor), so the funnel can scope its top-of-funnel denominator to anon traffic without depending on the client-only wpcomWriteIsAnon flag. READ-560 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bej2wXzZnmKgdR6v5UHc5J * Write: also register anon write-start on title input The title field binds to updateTitle, not repairStructure, so a visitor who typed only a title would emit anon_publish_click without a preceding anon_write_start. Hook the title action too, so a title-only author still registers a write-start. READ-560 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bej2wXzZnmKgdR6v5UHc5J * Write: flush anon publish_click Tracks pixel before the signup redirect wpcom Tracks sends beacons via new Image(), whose in-flight GET the browser cancels on unload. wpcom_write_editor_anon_publish_click fires immediately before window.location.assign() to the signup flow, so a synchronous redirect could drop the funnel's key conversion event. Route it through a bounded flush helper that gives the pixel time to dispatch, then navigate. The wait only applies when the Tracks client has upgraded the _tkq queue; otherwise it resolves immediately so the handoff is never stalled for a beacon that won't fire. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WHatptuGFWEvGfGzPDUjfT --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Committed via a GitHub action: https://github.com/Automattic/jetpack/actions/runs/28818337048 Upstream-Ref: Automattic/jetpack@28b9f52
1 parent 41c9822 commit 10e28c6

5 files changed

Lines changed: 158 additions & 38 deletions

File tree

jetpack_vendor/automattic/jetpack-mu-wpcom/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ This is an alpha version! The changes listed here are not final.
4545
- Show the Help Center icon on the navbar when seeing the Editor with a mobile device.
4646
- Smart Dictation: Add endpoints to proxy client secret requests.
4747
- WP.com smart dictation app
48+
- Write: add anon funnel Tracks events (write-start, publish-click) and an is_anon flag on editor open, for the Write On Phase 1 fake-door funnel.
4849
- Write: Add a topbar three-dot menu with "Open in block editor" and "Preview" actions when editing an existing post.
4950
- Write: Add blockquote citation support
5051
- Write: add full-justification alignment for paragraphs.

jetpack_vendor/automattic/jetpack-mu-wpcom/src/features/write/view.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,85 @@ function isAnon() {
3737
return typeof window !== 'undefined' && window.wpcomWriteIsAnon === true;
3838
}
3939

40+
// Approximates the anon editor open — the module loads immediately after the
41+
// server rendered the page (and fired `wpcom_write_editor_open`). Used as the
42+
// baseline for `time_to_publish_ms` on the anon publish-click event.
43+
const ANON_EDITOR_OPENED_AT = typeof Date !== 'undefined' ? Date.now() : 0;
44+
45+
// Guards `wpcom_write_editor_anon_write_start` to once per session.
46+
let anonWriteStartTracked = false;
47+
48+
/**
49+
* Fire a client-side Tracks event via the `_tkq` queue. Anon callers share the
50+
* `tk_ai` cookie, so these stitch to the eventual user at signup completion.
51+
*
52+
* @param {string} name - Tracks event name.
53+
* @param {object} [props] - Optional event properties.
54+
*/
55+
function recordTracksEvent( name, props ) {
56+
try {
57+
window._tkq = window._tkq || [];
58+
window._tkq.push( [ 'recordEvent', name, props || {} ] );
59+
} catch {
60+
// Tracks unavailable — nothing useful to do; swallow.
61+
}
62+
}
63+
64+
// How long to let a Tracks pixel leave the browser before a redirect. wpcom's
65+
// Tracks client sends via `new Image()`, and the browser cancels in-flight
66+
// image GETs on unload — so an event fired immediately before navigation can be
67+
// dropped. 250ms is imperceptible ahead of a full-page handoff to the signup
68+
// flow, and well within the round-trip a fire-and-forget pixel needs.
69+
const TRACKS_BEACON_FLUSH_MS = 250;
70+
71+
/**
72+
* Fire a Tracks event, then resolve once the pixel has had time to leave the
73+
* browser — for events recorded immediately before navigating away (e.g. the
74+
* anon publish handoff), where a synchronous redirect would otherwise cancel
75+
* the in-flight `new Image()` beacon. Best-effort and bounded: if the Tracks
76+
* client hasn't upgraded the queue (nothing will send), it resolves at once so
77+
* the handoff is never delayed for a beacon that won't fire.
78+
*
79+
* @param {string} name - Tracks event name.
80+
* @param {object} [props] - Optional event properties.
81+
* @return {Promise<void>} Resolves when it is safe to navigate.
82+
*/
83+
function recordTracksEventBeforeUnload( name, props ) {
84+
recordTracksEvent( name, props );
85+
86+
// The real Tracks client replaces `_tkq.push`; until it does, pushes just
87+
// pile up in a plain array and no pixel is sent, so there's nothing to wait
88+
// for. (This is exactly the pre-loader state we saw on the anon surface.)
89+
const tracksActive =
90+
typeof window !== 'undefined' && !! window._tkq && window._tkq.push !== Array.prototype.push;
91+
92+
if ( ! tracksActive || typeof setTimeout === 'undefined' ) {
93+
return Promise.resolve();
94+
}
95+
96+
return new Promise( resolve => {
97+
setTimeout( resolve, TRACKS_BEACON_FLUSH_MS );
98+
} );
99+
}
100+
101+
/**
102+
* Fire `wpcom_write_editor_anon_write_start` once, the first time an anon
103+
* visitor types real content. Keeps the funnel's write-through step honest —
104+
* an empty editor that's opened and abandoned shouldn't count as a write.
105+
*
106+
* @param {string} text - Current plain-text content of the editor.
107+
*/
108+
function maybeTrackAnonWriteStart( text ) {
109+
if ( anonWriteStartTracked || ! isAnon() ) {
110+
return;
111+
}
112+
if ( ! text || ! text.trim() ) {
113+
return;
114+
}
115+
anonWriteStartTracked = true;
116+
recordTracksEvent( 'wpcom_write_editor_anon_write_start' );
117+
}
118+
40119
/**
41120
* Persist the current draft snapshot to localStorage under the anon key.
42121
*
@@ -3458,6 +3537,13 @@ const { state } = store( 'wpcom-write', {
34583537
el.ref.style.height = 'auto';
34593538
el.ref.style.height = el.ref.scrollHeight + 'px';
34603539

3540+
// Typing a title counts as the first anon write too — the title field
3541+
// binds to this action rather than repairStructure, so hook it here so
3542+
// a title-only author still registers a write-start before publish.
3543+
if ( isAnon() && ! anonWriteStartTracked ) {
3544+
maybeTrackAnonWriteStart( state.title );
3545+
}
3546+
34613547
// Dismiss the recovery banner once the user starts editing.
34623548
if ( state.showRecoveryBanner ) {
34633549
localStorage.removeItem( AUTOSAVE_STORAGE_KEY );
@@ -3673,6 +3759,12 @@ const { state } = store( 'wpcom-write', {
36733759
promoteGapAtCursor();
36743760
ensureBlockStructure();
36753761
pushToUndoHistoryDebounced();
3762+
3763+
// First real keystroke in the anon funnel — fires once per session.
3764+
if ( isAnon() && ! anonWriteStartTracked ) {
3765+
const contentEl = getContent();
3766+
maybeTrackAnonWriteStart( contentEl ? contentEl.textContent : '' );
3767+
}
36763768
},
36773769

36783770
undo() {
@@ -5782,6 +5874,16 @@ const { state } = store( 'wpcom-write', {
57825874

57835875
async publish() {
57845876
if ( isAnon() ) {
5877+
// The publish-intent event — the moment an anon visitor hits the
5878+
// signup wall. Captured before navigating away so it isn't lost to
5879+
// the handoff. word_count / draft_size_bytes size the draft; the
5880+
// server open event and this share the tk_ai identity for stitching.
5881+
const contentEl = getContent();
5882+
const rawHtml = contentEl ? contentEl.innerHTML : '';
5883+
const plainText = contentEl ? contentEl.textContent || '' : '';
5884+
const words = plainText.trim() ? plainText.trim().split( /\s+/ ).length : 0;
5885+
const draftContent = rawHtml ? convertToBlocks( rawHtml ) : '';
5886+
57855887
// Flush the latest draft snapshot before navigating — autosave is
57865888
// on a 30s tick, and a fast typer-then-clicker would otherwise
57875889
// hand off stale (or no) content to the signup flow.
@@ -5791,6 +5893,18 @@ const { state } = store( 'wpcom-write', {
57915893
// internal navigation in this file does (cf. openInBlockEditor).
57925894
allowLeave = true;
57935895

5896+
// Record publish-intent and let the pixel dispatch before the
5897+
// redirect. wpcom Tracks beacons via `new Image()`, whose in-flight
5898+
// GET the browser cancels on unload — so a synchronous navigate
5899+
// would drop this event. The wait is bounded (and skipped entirely
5900+
// when Tracks isn't loaded) so the handoff is never stalled.
5901+
await recordTracksEventBeforeUnload( 'wpcom_write_editor_anon_publish_click', {
5902+
word_count: words,
5903+
time_to_publish_ms: Date.now() - ANON_EDITOR_OPENED_AT,
5904+
draft_size_bytes:
5905+
typeof Blob !== 'undefined' ? new Blob( [ draftContent ] ).size : draftContent.length,
5906+
} );
5907+
57945908
// Anon visitors hand off to the signup flow, which reads the draft
57955909
// from localStorage and publishes after signup completes.
57965910
window.location.assign( 'https://wordpress.com/setup/write-on' );

jetpack_vendor/automattic/jetpack-mu-wpcom/src/features/write/write.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,11 @@ function wpcom_write_render_admin_page() {
817817
$event_props = array(
818818
'is_new_post' => (int) ( 0 === $edit_post_id ),
819819
'source' => $source,
820+
// Anon entry is the only logged-out render of this editor (the wp-admin
821+
// page requires auth), so logged-out is a reliable proxy for the anon
822+
// fake-door funnel. Lets the funnel scope its top-of-funnel denominator
823+
// to anon traffic without depending on the client-only wpcomWriteIsAnon flag.
824+
'is_anon' => (int) ! is_user_logged_in(),
820825
);
821826

822827
if ( $edit_post_id > 0 ) {

jetpack_vendor/i18n-map.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
),
6363
'jetpack-mu-wpcom' => array(
6464
'path' => 'jetpack_vendor/automattic/jetpack-mu-wpcom',
65-
'ver' => '6.11.0-alpha1783360822',
65+
'ver' => '6.11.0-alpha1783366870',
6666
),
6767
'jetpack-password-checker' => array(
6868
'path' => 'jetpack_vendor/automattic/jetpack-password-checker',

0 commit comments

Comments
 (0)