Skip to content

Commit bc5900c

Browse files
committed
feat(metrics): markdown editor usage instrumentation
Adds new EVENT_TYPE.MD = "md" and instruments the high-value markdown editor surfaces. All payloads are metadata-only — no markdown content, file paths, image data, or user keystrokes cross any boundary. Bridge: - New mdviewrMetric event in MarkdownSync routes { kind, subcat, label, value? } from the iframe to Metrics.countEvent / valueEvent under EVENT_TYPE.MD with type validation. - src-mdviewer/src/bridge.js exports metricCount / metricValue helpers iframe modules call directly. Host-side metrics (MarkdownSync.js + main.js + EditorCommandHandlers): - md/theme/{dark,light} initial theme on iframe-ready, plus every mdviewrThemeToggle. - md/mode/{edit,reader} every mode transition. - md/doc/edited at most once per file per session (first iframe content change). - md/upsell/shown live-preview md edit upsell opens for a free user (mirror of the existing lp-edit/mdEditUpsell/show metric). - md/image/pasteCM image pasted in the CodeMirror markdown editor. Iframe-side metrics (via mdviewrMetric): - md/nav/formatClick bold / italic / strike / underline / inline code / heading dropdown. - md/nav/itemClick every other top-toolbar button (link, lists, task, quote, hr, table, codeblock, image-url, image-upload). - md/image/insert image-insert toolbar buttons (paired with the itemClick above for cross- feature roll-up). - md/image/pasteLP image paste inside the LP iframe. - md/print/click print toolbar button. - md/slash/popup slash menu becomes visible. - md/slash/select a slash item was picked (no item id recorded; recordUsage already tracks local prioritisation). - md/table/{rowEdit,colEdit} any row-handle / column-handle menu action — single bucket per type so we keep cardinality low.
1 parent 21120ed commit bc5900c

8 files changed

Lines changed: 121 additions & 0 deletions

File tree

src-mdviewer/src/bridge.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,3 +1430,16 @@ function sendToParent(eventName, payload) {
14301430
...payload
14311431
}, "*");
14321432
}
1433+
1434+
// ─── Metrics helpers ───────────────────────────────────────────────
1435+
// Forwards metric events to the host (MarkdownSync.js) which routes them
1436+
// to Metrics.countEvent / valueEvent under EVENT_TYPE.MD. Only metadata
1437+
// (subcat, label, optional numeric value) crosses the boundary — never
1438+
// markdown content, file paths, or user keystrokes.
1439+
export function metricCount(subcat, label) {
1440+
sendToParent("mdviewrMetric", { kind: "count", subcat, label });
1441+
}
1442+
1443+
export function metricValue(subcat, label, value) {
1444+
sendToParent("mdviewrMetric", { kind: "value", subcat, label, value });
1445+
}

src-mdviewer/src/components/editor.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { initImagePopover, destroyImagePopover } from "./image-popover.js";
1212
import { initLangPicker, destroyLangPicker, isLangPickerDropdownOpen } from "./lang-picker.js";
1313
import { highlightCode, renderAfterHTML, normalizeCodeLanguages } from "./viewer.js";
1414
import { initMermaidEditor, destroyMermaidEditor, insertMermaidBlock, attachOverlays } from "./mermaid-editor.js";
15+
import { metricCount } from "../bridge.js";
1516

1617
const devLog = import.meta.env.DEV ? console.log.bind(console, "[editor]") : () => {};
1718
let turndown = null;
@@ -713,6 +714,10 @@ function handleImagePaste(e, contentEl) {
713714
for (let i = 0; i < items.length; i++) {
714715
if (items[i].kind === "file" && ALLOWED_IMAGE_TYPES.includes(items[i].type)) {
715716
e.preventDefault();
717+
// Image paste in the live-preview iframe (paired with
718+
// md/image/pasteCM for the CodeMirror editor path). No
719+
// image data is recorded — just the count.
720+
metricCount("image", "pasteLP");
716721
const blob = items[i].getAsFile();
717722
const fileName = blob.name || ("image." + blob.type.split("/")[1]);
718723
const uploadId = _insertUploadPlaceholder(contentEl);
@@ -1005,6 +1010,17 @@ function showHandleMenu(anchor, type, ctx, contentEl, wrapper, clickX) {
10051010
];
10061011
}
10071012

1013+
// Wrap each action so picking any row/col menu item raises a single
1014+
// metric (md/table/rowEdit or colEdit). We don't record which
1015+
// specific edit — just that one happened — to keep cardinality low.
1016+
const editLabel = type === "row" ? "rowEdit" : "colEdit";
1017+
items.forEach((it) => {
1018+
if (it.action) {
1019+
const orig = it.action;
1020+
it.action = () => { metricCount("table", editLabel); orig(); };
1021+
}
1022+
});
1023+
10081024
menu.innerHTML = "";
10091025
items.forEach((item) => {
10101026
if (item.divider) {

src-mdviewer/src/components/embedded-toolbar.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ import {
4040
import { on, emit } from "../core/events.js";
4141
import { getState, setState } from "../core/state.js";
4242
import { t, tp } from "../core/i18n.js";
43+
import { metricCount } from "../bridge.js";
44+
45+
// Toolbar buttons whose clicks roll up under md/nav/formatClick (text-
46+
// formatting and heading switches). Anything not in this set is treated
47+
// as a generic md/nav/itemClick so we capture toolbar usage without
48+
// fragmenting the metric across every button id.
49+
const FORMAT_CLICK_IDS = new Set([
50+
"emb-bold", "emb-italic", "emb-strike", "emb-underline", "emb-code",
51+
"emb-block-type" // heading dropdown
52+
]);
4353

4454
let toolbar = null;
4555
let resizeObserver = null;
@@ -264,6 +274,14 @@ function wireFormatButtons() {
264274
if (el) {
265275
el.addEventListener("mousedown", (e) => {
266276
e.preventDefault();
277+
// Generic toolbar usage metric: format vs item bucket.
278+
// Image insert specifically is also tracked under
279+
// md/image/insert below for cross-feature roll-up.
280+
metricCount("nav",
281+
FORMAT_CLICK_IDS.has(binding.id) ? "formatClick" : "itemClick");
282+
if (binding.id === "emb-image-url" || binding.id === "emb-image-upload") {
283+
metricCount("image", "insert");
284+
}
267285
emit("action:format", { command: binding.command, value: binding.value });
268286
});
269287
}
@@ -274,6 +292,9 @@ function wireBlockTypeSelect() {
274292
const blockTypeSelect = document.getElementById("emb-block-type");
275293
if (blockTypeSelect) {
276294
blockTypeSelect.addEventListener("change", (e) => {
295+
// Heading dropdown rolls up under formatClick (heading
296+
// selection is a formatting action, just delivered via select).
297+
metricCount("nav", "formatClick");
277298
emit("action:format", { command: "formatBlock", value: e.target.value });
278299
e.target.blur();
279300
});
@@ -347,6 +368,7 @@ function wirePrintButton() {
347368
const printBtn = document.getElementById("emb-print-btn");
348369
if (printBtn) {
349370
printBtn.addEventListener("click", () => {
371+
metricCount("print", "click");
350372
window.print();
351373
});
352374
}

src-mdviewer/src/components/slash-menu.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { emit } from "../core/events.js";
2121
import { getSelectionRect } from "./editor.js";
2222
import { t } from "../core/i18n.js";
23+
import { metricCount } from "../bridge.js";
2324

2425
let menu = null;
2526
let contentEl = null;
@@ -221,6 +222,9 @@ function show() {
221222
const rect = _savedSlashRect;
222223
const anchor = document.getElementById("slash-menu-anchor");
223224
if (!anchor) return;
225+
// Slash popup appearance metric — fires when the menu actually
226+
// becomes visible, not on every keystroke that filters items.
227+
metricCount("slash", "popup");
224228

225229
// Position anchor below the cursor line with gap
226230
const lineHeight = rect.bottom - rect.top;
@@ -273,6 +277,9 @@ function selectItem(index) {
273277
if (index < 0 || index >= filteredItems.length) return;
274278
const item = filteredItems[index];
275279
recordUsage(item.labelKey);
280+
// Slash selection metric — does not record which item was picked
281+
// (recordUsage already tracks that locally for prioritisation).
282+
metricCount("slash", "select");
276283

277284
// Capture slashRange before hide() clears it
278285
const savedRange = slashRange;

src/editor/EditorCommandHandlers.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ define(function (require, exports, module) {
4141
ChangeHelper = require("editor/EditorHelper/ChangeHelper"),
4242
LanguageManager = require("language/LanguageManager"),
4343
ImageUploadManager = require("features/ImageUploadManager"),
44+
Metrics = require("utils/Metrics"),
4445
Dialogs = require("widgets/Dialogs"),
4546
AppInit = require("utils/AppInit");
4647

@@ -1316,6 +1317,11 @@ define(function (require, exports, module) {
13161317

13171318
event.preventDefault();
13181319

1320+
// Metric: image paste in the CodeMirror markdown editor view
1321+
// (paired with md/image/pasteLP for the live-preview iframe path).
1322+
// No image data or filename is recorded — just the count.
1323+
Metrics.countEvent(Metrics.EVENT_TYPE.MD, "image", "pasteCM");
1324+
13191325
const blob = imageItem.getAsFile();
13201326
const fileName = blob.name || ("image." + blob.type.split("/")[1]);
13211327
const provider = ImageUploadManager.getImageUploadProvider();

src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ define(function (require, exports, module) {
2929
CommandManager = require("command/CommandManager"),
3030
Commands = require("command/Commands"),
3131
KeyBindingManager = require("command/KeyBindingManager"),
32+
Metrics = require("utils/Metrics"),
3233
utils = require("./utils");
3334

3435
// Commands whose shortcuts, when forwarded from the md viewer iframe,
@@ -42,6 +43,11 @@ define(function (require, exports, module) {
4243
Commands.CMD_FIND_IN_FILES
4344
];
4445

46+
// Set of file paths the user has edited in the markdown viewer this
47+
// session. Used to fire md/doc/edited at most once per file so the
48+
// metric tells us "users who edit at least one md doc" without
49+
// inflating with every keystroke.
50+
const _mdEditedFiles = new Set();
4551
let _active = false;
4652
let _doc = null;
4753
let _$iframe = null;
@@ -127,6 +133,12 @@ define(function (require, exports, module) {
127133
_handleRedo();
128134
break;
129135
case "mdviewrEditModeChanged":
136+
// Mode switch metric — reader/edit are the only two
137+
// states the iframe reports. Fires on every transition
138+
// (including init) so we see how often users go in and
139+
// out of edit mode.
140+
Metrics.countEvent(Metrics.EVENT_TYPE.MD, "mode",
141+
data.editMode ? "edit" : "reader");
130142
// When switching to reader, send CM content so the iframe
131143
// can re-render with accurate data-source-line for cursor sync.
132144
if (!data.editMode && _doc) {
@@ -161,6 +173,30 @@ define(function (require, exports, module) {
161173
if (_onThemeToggle) {
162174
_onThemeToggle(data.theme);
163175
}
176+
// Theme switch metric — fires on every user toggle. The
177+
// initial state (light vs dark) is reported separately
178+
// when the viewer first activates a markdown doc.
179+
if (data.theme === "dark" || data.theme === "light") {
180+
Metrics.countEvent(Metrics.EVENT_TYPE.MD, "theme", data.theme);
181+
}
182+
break;
183+
case "mdviewrMetric":
184+
// Generic metric forwarder for events the iframe wants
185+
// to record. Schema:
186+
// { kind: "count" | "value", subcat, label, [value] }
187+
// We validate types before forwarding so a malformed
188+
// payload from the iframe can't break the metrics
189+
// pipeline. Category is always EVENT_TYPE.MD.
190+
if (data && typeof data.subcat === "string" && data.subcat &&
191+
typeof data.label === "string" && data.label) {
192+
if (data.kind === "value" && Number.isFinite(data.value)) {
193+
Metrics.valueEvent(Metrics.EVENT_TYPE.MD,
194+
data.subcat, data.label, data.value);
195+
} else {
196+
Metrics.countEvent(Metrics.EVENT_TYPE.MD,
197+
data.subcat, data.label);
198+
}
199+
}
164200
break;
165201
case "mdviewrImageUploadRequest":
166202
_handleImageUploadFromIframe(data);
@@ -390,6 +426,15 @@ define(function (require, exports, module) {
390426
_sendTheme();
391427
_sendLocale();
392428
_sendSkipRefocusShortcuts();
429+
// Record the effective theme the user is starting in
430+
// (light vs dark). Mirror of mdviewrThemeToggle so the dark/
431+
// light counter reflects both initial state and toggles.
432+
const initialTheme = _themeOverride
433+
|| ((ThemeManager.getCurrentTheme() && ThemeManager.getCurrentTheme().dark)
434+
? "dark" : "light");
435+
if (initialTheme === "dark" || initialTheme === "light") {
436+
Metrics.countEvent(Metrics.EVENT_TYPE.MD, "theme", initialTheme);
437+
}
393438
if (_onIframeReadyCallback) {
394439
_onIframeReadyCallback();
395440
}
@@ -622,6 +667,14 @@ define(function (require, exports, module) {
622667

623668
_applyDiffToEditor(markdown);
624669

670+
// First-edit metric per file per session — fires at most once
671+
// per document so "users who edit any md file" is a clean count.
672+
const editedPath = _doc && _doc.file && _doc.file.fullPath;
673+
if (editedPath && !_mdEditedFiles.has(editedPath)) {
674+
_mdEditedFiles.add(editedPath);
675+
Metrics.countEvent(Metrics.EVENT_TYPE.MD, "doc", "edited");
676+
}
677+
625678
// Send back the actual CM text so the iframe can compute accurate
626679
// data-source-line attributes. The markdown from convertToMarkdown
627680
// may differ slightly from CM's content (e.g. table formatting),

src/extensionsIntegrated/Phoenix-live-preview/main.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,6 +1626,9 @@ define(function (require, exports, module) {
16261626
text: buttonGetProText }
16271627
];
16281628
Metrics.countEvent(Metrics.EVENT_TYPE.LP_EDIT, "mdEditUpsell", "show");
1629+
// Mirror under the MD category so it groups with the rest
1630+
// of the markdown-editor metrics on the dashboard.
1631+
Metrics.countEvent(Metrics.EVENT_TYPE.MD, "upsell", "shown");
16291632
Dialogs.showModalDialog(Dialogs.DIALOG_ID_INFO,
16301633
Strings.AVAILABLE_IN_PRO_TITLE,
16311634
Strings.MD_EDIT_UPSELL_MESSAGE, buttons)

src/utils/Metrics.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ define(function (require, exports, module) {
131131
PRO: "pro",
132132
AI: "ai",
133133
TERMINAL: "term",
134+
MD: "md",
134135
GUIDE: "guide"
135136
};
136137

0 commit comments

Comments
 (0)