@inizioevoke/veeva-astroclm-core 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,441 @@
1
+ ---
2
+ name: analytics-auditor
3
+ description: >
4
+ Starts the Astro dev server, visits every page, clicks configurable CSS selectors,
5
+ captures all browser console messages, and writes a deduplication report of unique
6
+ log messages grouped by page. Use this skill whenever the user asks to audit,
7
+ record, or report on analytics, metrics, or events.
8
+ ---
9
+
10
+ # Analytics Auditor Skill
11
+
12
+ ## Purpose
13
+
14
+ Start the dev server, click interactive elements, harvest every `console.*` message,
15
+ and produce a timestamped report of **unique messages** (grouped by page) written to
16
+ `analytics-audit-report-<timestamp>.md` in the project root.
17
+
18
+ ---
19
+
20
+ ## Instructions
21
+
22
+ ### Step 0 — Load the Playwright MCP tool schemas
23
+
24
+ Before doing anything else, call:
25
+ ```
26
+ ToolSearch({ query: "select:mcp__plugin_playwright_playwright__browser_navigate,mcp__plugin_playwright_playwright__browser_click,mcp__plugin_playwright_playwright__browser_console_messages,mcp__plugin_playwright_playwright__browser_snapshot,mcp__plugin_playwright_playwright__browser_wait_for,mcp__plugin_playwright_playwright__browser_evaluate,mcp__plugin_playwright_playwright__browser_handle_dialog,mcp__plugin_playwright_playwright__browser_close" })
27
+ ```
28
+
29
+ You will need all of those tools to be loaded and callable.
30
+
31
+ ---
32
+
33
+ ### Step 1 — Start the dev server and detect its port
34
+
35
+ Always start a fresh dev server. Use a timestamped log filename so the poll can never
36
+ accidentally read the port from a previously running server's log:
37
+
38
+ ```bash
39
+ ASTRO_LOG="/tmp/astro-dev-$(date +%s).log"
40
+ npm run dev > "$ASTRO_LOG" 2>&1 &
41
+ echo "$ASTRO_LOG"
42
+ ```
43
+
44
+ Save the log path printed by the `echo` — you will need it for polling and cleanup.
45
+
46
+ Then poll the log file until Astro prints its ready URL. **Do not use `grep -P`** —
47
+ it is not supported on Windows. Use this portable polling loop instead:
48
+
49
+ ```bash
50
+ port=""
51
+ for i in $(seq 1 30); do
52
+ port=$(grep -o 'localhost:[0-9]*' "$ASTRO_LOG" | head -1 | grep -o '[0-9]*$')
53
+ if [ -n "$port" ]; then echo "$port"; break; fi
54
+ sleep 1
55
+ done
56
+ if [ -z "$port" ]; then
57
+ echo "ERROR: dev server did not produce a port within 30 seconds" >&2
58
+ cat "$ASTRO_LOG" >&2
59
+ exit 1
60
+ fi
61
+ ```
62
+
63
+ If the loop exits without a port, stop and report the error to the user — do not
64
+ continue with subsequent steps.
65
+
66
+ Extract the port from that output and use it as `DEV_PORT` for all subsequent URLs
67
+ (e.g. `http://localhost:${DEV_PORT}/home/`).
68
+
69
+ Once the port has been extracted, delete the log file:
70
+ ```bash
71
+ rm -f "$ASTRO_LOG"
72
+ ```
73
+
74
+ ---
75
+
76
+ ### Step 2 — Capture timestamp, discover pages, and define click selectors
77
+
78
+ #### Report timestamp
79
+
80
+ Before starting any browser work, generate the timestamp that will be used in the
81
+ report filename. Use `browser_evaluate` once the browser is open, or compute it via
82
+ any available JS execution context:
83
+
84
+ ```js
85
+ new Date().toISOString().replace(/[^\d]/g, '').substring(0, 14)
86
+ ```
87
+
88
+ Store the result as **`TIMESTAMP`** (e.g. `20260616143022`). It is used once in Step 5
89
+ to name the output file.
90
+
91
+ #### Page URLs
92
+
93
+ Do **not** hardcode the page list. Instead, read the `src/pages` directory to discover
94
+ all `.astro` files:
95
+ ```bash
96
+ find src/pages -name "*.astro" | sort
97
+ ```
98
+
99
+ **Skip any file whose name contains `[` or `]`** — these are dynamic route templates
100
+ that require parameter values and cannot be visited directly. Record each skipped file
101
+ in a list for the report's header section.
102
+
103
+ Convert each remaining file path to a URL route using these rules:
104
+ - Strip the `src/pages/` prefix and `.astro` extension
105
+ - A file named `index.astro` maps to `/`
106
+ - All other files map to `/<name>/` (trailing slash)
107
+ - Nested files (e.g. `src/pages/about/moa.astro`) map to `/about/moa/`
108
+
109
+ Prepend `http://localhost:${DEV_PORT}` from Step 1 to form the full URL.
110
+
111
+ Example — `src/pages/home.astro` → `http://localhost:${DEV_PORT}/home/`
112
+
113
+ #### Click selectors
114
+
115
+ **Before finalizing the selector list, read `src/lib/client/main.ts`** (and any other
116
+ files it imports from `src/lib/client/`) and check whether `IVA.configure()` or
117
+ `IVA.init()` is called with a config object that contains any of these properties:
118
+
119
+ - **`selectors.clickable`** — if present, it is a CSS selector string that replaces
120
+ the non-`[data-evotrk]` selectors in the default list. Replace those defaults with
121
+ this value, but **keep `[data-evotrk]`** (or the `attrs.data`-derived selector, see
122
+ below) in the final list.
123
+
124
+ - **`attrs.data`** — if present, it is the attribute name used instead of
125
+ `data-evotrk` to mark tracked elements. It may or may not include surrounding
126
+ brackets. Normalize it: if it already starts with `[`, use it as-is; otherwise wrap
127
+ it as `[<value>]`. Replace `[data-evotrk]` in the selector list with this normalized
128
+ value.
129
+
130
+ - **`attrs.navigateDisabled`** — if present, it is the attribute name IVA checks to
131
+ decide whether to skip its `navigate()` call. Store this value (without brackets)
132
+ as **`NAVIGATE_DISABLED_ATTR`** for use in Step 3. Default if absent:
133
+ `data-navigate-disabled`.
134
+
135
+ - **`attrs.gotoslide`** — if present, it is the attribute name IVA uses to identify
136
+ which `<a>` elements it has bound its click handler to. Store this value (without
137
+ brackets) as **`GOTO_SLIDE_ATTR`** for use in Step 3. Default if absent:
138
+ `data-gotoslide`.
139
+
140
+ If none of these properties are found, use this default list unchanged:
141
+
142
+ ```
143
+ [data-evotrk]
144
+ a
145
+ button
146
+ [role="button"]
147
+ [role="link"]
148
+ [role="tab"]
149
+ input[type="checkbox"]
150
+ input[type="radio"]
151
+ [role="checkbox"]
152
+ [role="radio"]
153
+ ```
154
+
155
+ On every page, query for all elements matching the final selector list and click each
156
+ one that is visible and not disabled.
157
+
158
+ Use a single combined CSS selector string joined with commas when querying the page.
159
+ The user may supply additional or replacement selectors in their request — merge them
160
+ into this list rather than replacing it entirely unless the user says otherwise.
161
+
162
+ ---
163
+
164
+ ### Step 3 — Per-page procedure
165
+
166
+ For **each page URL** discovered in Step 2, also initialize a **`pageMessages`** list
167
+ (reset to empty for each new page) that accumulates all console messages seen on the
168
+ current page.
169
+
170
+ 1. Navigate to the page URL.
171
+ 2. Wait for the page to load (use `browser_wait_for` with a known selector like `h1`).
172
+ 3. **Disable navigation on all links** using `browser_evaluate`. Pass
173
+ `NAVIGATE_DISABLED_ATTR` as the attribute name. This approach is reliable because
174
+ it works at the DOM level before any click handler fires:
175
+
176
+ - **IVA-bound links** — if the `<a>` element has the `GOTO_SLIDE_ATTR` attribute,
177
+ IVA has registered its click handler on it. Set `<NAVIGATE_DISABLED_ATTR>="true"`
178
+ on the element so IVA skips its `navigate()` call. IVA already calls
179
+ `e.preventDefault()` unconditionally, so browser default navigation is also
180
+ suppressed.
181
+
182
+ - **All other links** — if the `<a>` element does not have `GOTO_SLIDE_ATTR`
183
+ (external links, PDF links, plain anchors, etc.), remove the `href` attribute
184
+ entirely (store the original value in `data-original-href`). Without an `href`
185
+ the browser will not navigate; any IVA tracking handler still fires normally.
186
+
187
+ Use this script (substitute the actual attribute names for the placeholders):
188
+ ```js
189
+ (gotoSlideAttr, navigateDisabledAttr) => {
190
+ document.querySelectorAll('a').forEach(a => {
191
+ if (a.hasAttribute(gotoSlideAttr)) {
192
+ // IVA-bound link — use IVA's own escape hatch
193
+ a.setAttribute(navigateDisabledAttr, 'true');
194
+ } else {
195
+ // Not IVA-bound — strip href to prevent browser navigation
196
+ const href = a.getAttribute('href');
197
+ if (href) {
198
+ a.dataset.originalHref = href;
199
+ a.removeAttribute('href');
200
+ }
201
+ }
202
+ });
203
+ }
204
+ ```
205
+
206
+ Call this evaluate **after** the page loads (Step 3.2) and **again after any
207
+ DOM mutation** (modal open, tab switch, dynamic content) that may have added new
208
+ `<a>` elements.
209
+
210
+ 4. Take a `browser_snapshot` to get the current accessibility tree. Use the refs from
211
+ this snapshot for all subsequent clicks on this page.
212
+
213
+ 5. For **each matched element** (visible, not disabled):
214
+ a. Click it using `browser_click`.
215
+ b. Wait 500ms for async effects.
216
+ c. **Call `browser_console_messages` with `level: "debug"`** to get all messages
217
+ accumulated on this page so far. Identify the **newly appeared messages** by
218
+ comparing the full returned list against `pageMessages` (new = any message whose
219
+ de-timestamped text is not already present in `pageMessages`). Add all new
220
+ messages to `pageMessages`.
221
+ d. Take a `browser_snapshot`. If the tree is structurally unchanged from the
222
+ previous snapshot, the existing refs remain valid — no action needed. If the
223
+ tree changed, use the new snapshot's refs going forward.
224
+ e. If the snapshot shows a modal is now open (`Modal state` present in the tree):
225
+ i. Use `browser_evaluate` to collect identifying information for all clickable
226
+ elements inside the modal:
227
+ ```js
228
+ () => {
229
+ const modal = document.querySelector('[role="dialog"], [aria-modal="true"]');
230
+ if (!modal) return [];
231
+ const sel = 'a,button,[role="button"],[role="link"],[role="tab"],input[type="checkbox"],input[type="radio"],[role="checkbox"],[role="radio"]';
232
+ return Array.from(modal.querySelectorAll(sel))
233
+ .filter(el => !el.disabled && el.offsetParent !== null)
234
+ .map(el => el.getAttribute('aria-label') || el.textContent.trim() || el.tagName);
235
+ }
236
+ ```
237
+ Cross-reference the returned labels/text against the current snapshot to
238
+ identify the corresponding accessibility-tree refs for clicking.
239
+ ii. For each visible, non-disabled modal element: click it using its ref, wait
240
+ 500ms, then apply the same **step 5c** above (call `browser_console_messages`
241
+ and diff). Re-snapshot if the modal content changes.
242
+ iii. After all modal elements have been clicked, dismiss the modal by clicking
243
+ its close button — look for a button with `aria-label="Close"` or visible
244
+ text "×", "Close", or "Dismiss" in the snapshot. Only use
245
+ `browser_handle_dialog` if a **native** browser dialog (`alert`/`confirm`/
246
+ `prompt`) is present — it has no effect on custom DOM modals.
247
+ iv. Take a fresh `browser_snapshot` after the modal closes before continuing
248
+ to the next page-level element — refs from before the modal opened are stale.
249
+ f. If no modal appeared but the DOM changed significantly, use the new snapshot's
250
+ refs before proceeding.
251
+
252
+ 6. After all clicks on the page, call `browser_console_messages` **once more** with
253
+ `level: "debug"` to catch any async messages that fired after the last click. Diff
254
+ against `pageMessages` and append any remaining new messages.
255
+
256
+ 7. Store all messages in `pageMessages` with the page route (e.g. `/home/`) as their
257
+ source.
258
+
259
+ ---
260
+
261
+ ### Step 4 — Parse, deduplicate, and structure the data
262
+
263
+ #### Identify tracking events
264
+
265
+ When an element is clicked the app emits a tracking log in this format:
266
+ ```
267
+ [HH:MM:SS.mmm] DEBUG: track {slide: 'home', type: 'button', name: 'button_evo_flip_card_trigger'}
268
+ ```
269
+
270
+ For every console message, check whether its text matches this pattern:
271
+ ```
272
+ DEBUG: track {…}
273
+ ```
274
+
275
+ If it matches, parse out the object payload `{ slide, type, name }` — these are the
276
+ primary data points of interest. Store them as structured tracking events separate from
277
+ general log messages.
278
+
279
+ #### Collect all messages
280
+
281
+ Gather every console message captured across all pages into a flat list:
282
+ ```
283
+ {
284
+ page: string, // route, e.g. "/home/"
285
+ level: "log"|"warn"|"error"|"info"|"debug",
286
+ text: string, // full raw message text
287
+ tracking: { // only present when the message is a tracking event
288
+ slide: string,
289
+ type: string,
290
+ name: string
291
+ } | null
292
+ }
293
+ ```
294
+
295
+ #### Deduplicate
296
+
297
+ Before comparing, **strip the leading timestamp prefix** from each message's `text`
298
+ (the `[HH:MM:SS.mmm]` portion) — timestamps differ per run and would prevent identical
299
+ messages from being recognized as duplicates. Compare only the de-timestamped text.
300
+
301
+ Two messages are duplicates if their de-timestamped texts are equal. When the same
302
+ message appears on multiple pages, record all pages it appeared on.
303
+
304
+ #### Classify global vs. slide-specific tracking events
305
+
306
+ After collecting all tracking events, split them into two groups. An event is **Global**
307
+ if **either** condition is true:
308
+
309
+ 1. Its `type` starts with `header-`, `footer-`, or `nav-`; **or**
310
+ 2. The `(type, name)` pair appears on **≥ 80%** of the total audited pages.
311
+
312
+ Everything else is a **Slide Event**.
313
+
314
+ For global events, drop the `slide` dimension entirely — only `type` and `name` are needed.
315
+ For slide events, retain `slide`, `type`, and `name`.
316
+
317
+ #### Tiers
318
+
319
+ Organize into four tiers:
320
+ 1. **Tracking Events** — messages matching the `DEBUG: track` pattern (split into Global and Slide sub-sections, see Step 5)
321
+ 2. **Errors** — `console.error`
322
+ 3. **Warnings** — `console.warn`
323
+ 4. **Info / Log** — `console.log`, `console.info`, `console.debug` (excluding tracking events)
324
+
325
+ ---
326
+
327
+ ### Step 5 — Write the report
328
+
329
+ Before inserting any message text into a Markdown table cell, **escape every `|`
330
+ character as `\|`** to prevent table corruption.
331
+
332
+ Write a Markdown file to `analytics-audit-report-<TIMESTAMP>.md` in the project root with this
333
+ structure:
334
+
335
+ ```markdown
336
+ # Browser Console Log Audit
337
+
338
+ **Date:** <today's date>
339
+ **Pages audited:** /home/, /about/, /about/moa/
340
+ **Total unique messages:** <N> tracking events (<G> global, <S> slide-specific), <E> errors, <W> warnings, <I> info/log
341
+
342
+ > **Skipped (dynamic routes):** src/pages/[slug].astro, src/pages/[...path].astro
343
+
344
+ ---
345
+
346
+ ## Tracking Events (<count>)
347
+
348
+ > Events are grouped by slide. Each slide's `type` and `name` are set by the IVA tracking library at click time; the `slide` value is defined by each page's own configuration.
349
+
350
+ ### Global Events (<G> — type prefixed `header-`/`footer-`/`nav-`, or present on ≥80% of pages)
351
+
352
+ These `(type, name)` pairs represent shared UI (navigation, ISI controls, header/footer
353
+ links, etc.). The slide name is omitted since it is not meaningful here.
354
+
355
+ | Type | Name |
356
+ |------|------|
357
+ | `nav-button` | `nav-button_about_pbc` |
358
+
359
+ ### Slide Events (<S> — page-specific)
360
+
361
+ These events fire on fewer than 80% of pages and are meaningful to a specific slide or
362
+ feature.
363
+
364
+ | Slide | Type | Name |
365
+ |-------|------|------|
366
+ | `home` | `button` | `button_important_safety_information` |
367
+
368
+ ## Errors (<count>)
369
+
370
+ | Message | Pages |
371
+ |---------|-------|
372
+ | `<message text>` | /home/, /about/ |
373
+
374
+ ## Warnings (<count>)
375
+
376
+ | Message | Pages |
377
+ |---------|-------|
378
+ | `<message text>` | /home/ |
379
+
380
+ ## Info / Log (<count>)
381
+
382
+ | Message | Pages |
383
+ |---------|-------|
384
+ | `<message text>` | /about/moa/ |
385
+
386
+ ---
387
+
388
+ *Generated by the analytics-auditor skill.*
389
+ ```
390
+
391
+ Omit the "Skipped" blockquote if no dynamic routes were found. If a tier has zero
392
+ messages, write `_None_` instead of a table. If there are no slide-specific events,
393
+ write `_None_` under that sub-section. Omit the Global Events sub-section header note
394
+ if there are no global events.
395
+
396
+ After writing the file, tell the user the full filename (e.g.
397
+ `analytics-audit-report-20260616143022.md`) and give a brief summary (e.g. "Found 8
398
+ tracking events (5 global, 3 slide-specific), 2 errors, 1 warning, 5 info messages
399
+ across 3 pages").
400
+
401
+ ---
402
+
403
+ ### Step 6 — Close the browser and clean up
404
+
405
+ Call `browser_close` when done so the Playwright session is cleaned up.
406
+
407
+ Then delete the `.playwright-mcp` directory that Playwright creates for snapshots and
408
+ console logs:
409
+ ```bash
410
+ rm -rf .playwright-mcp
411
+ ```
412
+
413
+ Do **not** stop the dev server — the user likely wants to keep it running.
414
+
415
+ ---
416
+
417
+ ## Notes
418
+
419
+ - The Astro dev server defaults to port **4321** but increments on collision — always
420
+ read the actual port from the server output as described in Step 1.
421
+ - **`grep -P` is not supported on Windows** — use `grep -o` with basic regex patterns.
422
+ - **Snapshot refs go stale** after DOM mutations (modal open/close, tab switches). Always
423
+ re-snapshot after any click that visibly changes the page structure.
424
+ - **`browser_console_messages` fails if a native dialog is showing** — dismiss with
425
+ `browser_handle_dialog` before calling it.
426
+ - **`browser_handle_dialog` only works for native browser dialogs** (`alert`, `confirm`,
427
+ `prompt`) — it has no effect on custom DOM modals. Dismiss those by clicking their
428
+ close button.
429
+ - **Navigation prevention uses DOM attributes, not event listeners.** IVA's click
430
+ handler calls `e.preventDefault()` unconditionally and then checks
431
+ `<navigateDisabledAttr>="true"` before calling `navigate()`. Setting that attribute
432
+ on IVA-bound links is therefore the most reliable way to suppress navigation without
433
+ interfering with tracking. All other links have their `href` removed instead.
434
+ - **Re-run the link-disabling evaluate after DOM mutations** (modal open, dynamic
435
+ tab content, etc.) — newly inserted `<a>` elements will not have the attribute yet.
436
+ - **`browser_console_messages` is called after every click** so that tracking events
437
+ are captured incrementally. The final per-page call catches any late async messages.
438
+ - If the user provides their own list of selectors or pages, replace the defaults in
439
+ Step 2 before proceeding.
440
+ - If a selector does not match any element on a page, log a warning in the report's
441
+ header section rather than failing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inizioevoke/veeva-astroclm-core",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "",