@inizioevoke/veeva-astroclm-core 1.1.1 → 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.
@@ -12,8 +12,8 @@ description: >
12
12
  ## Purpose
13
13
 
14
14
  Start the dev server, click interactive elements, harvest every `console.*` message,
15
- and produce a report of **unique messages** (grouped by page) written to
16
- `analytics-audit-report.md` in the project root.
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
17
 
18
18
  ---
19
19
 
@@ -32,34 +32,61 @@ You will need all of those tools to be loaded and callable.
32
32
 
33
33
  ### Step 1 — Start the dev server and detect its port
34
34
 
35
- Astro auto-increments the port when the default (4321) is already in use, so never
36
- assume a fixed port. Instead, capture the server's own output to read the URL it
37
- actually binds to.
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:
38
37
 
39
- Run this command to start the server and tee its output to a temp file:
40
38
  ```bash
41
- npm run dev > /tmp/astro-dev.log 2>&1 &
39
+ ASTRO_LOG="/tmp/astro-dev-$(date +%s).log"
40
+ npm run dev > "$ASTRO_LOG" 2>&1 &
41
+ echo "$ASTRO_LOG"
42
42
  ```
43
43
 
44
+ Save the log path printed by the `echo` — you will need it for polling and cleanup.
45
+
44
46
  Then poll the log file until Astro prints its ready URL. **Do not use `grep -P`** —
45
47
  it is not supported on Windows. Use this portable polling loop instead:
48
+
46
49
  ```bash
47
- for i in $(seq 1 20); do
48
- port=$(grep -o 'localhost:[0-9]*' /tmp/astro-dev.log | head -1 | grep -o '[0-9]*$')
49
- if [ -n "$port" ]; then echo $port; break; fi
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
50
54
  sleep 1
51
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
52
61
  ```
53
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
+
54
66
  Extract the port from that output and use it as `DEV_PORT` for all subsequent URLs
55
67
  (e.g. `http://localhost:${DEV_PORT}/home/`).
56
68
 
57
- If the log file already contains a port before you start the server, the server was
58
- already running — use that port and skip the `npm run dev` step.
69
+ Once the port has been extracted, delete the log file:
70
+ ```bash
71
+ rm -f "$ASTRO_LOG"
72
+ ```
59
73
 
60
74
  ---
61
75
 
62
- ### Step 2 — Discover pages and define click selectors
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.
63
90
 
64
91
  #### Page URLs
65
92
 
@@ -69,7 +96,11 @@ all `.astro` files:
69
96
  find src/pages -name "*.astro" | sort
70
97
  ```
71
98
 
72
- Convert each file path to a URL route using these rules:
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:
73
104
  - Strip the `src/pages/` prefix and `.astro` extension
74
105
  - A file named `index.astro` maps to `/`
75
106
  - All other files map to `/<name>/` (trailing slash)
@@ -81,10 +112,35 @@ Example — `src/pages/home.astro` → `http://localhost:${DEV_PORT}/home/`
81
112
 
82
113
  #### Click selectors
83
114
 
84
- On every page, query for all elements matching the following selectors and click each
85
- one that is visible and not disabled:
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:
86
141
 
87
142
  ```
143
+ [data-evotrk]
88
144
  a
89
145
  button
90
146
  [role="button"]
@@ -96,6 +152,9 @@ input[type="radio"]
96
152
  [role="radio"]
97
153
  ```
98
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
+
99
158
  Use a single combined CSS selector string joined with commas when querying the page.
100
159
  The user may supply additional or replacement selectors in their request — merge them
101
160
  into this list rather than replacing it entirely unless the user says otherwise.
@@ -104,25 +163,49 @@ into this list rather than replacing it entirely unless the user says otherwise.
104
163
 
105
164
  ### Step 3 — Per-page procedure
106
165
 
107
- For **each page URL** discovered in Step 2:
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.
108
169
 
109
170
  1. Navigate to the page URL.
110
171
  2. Wait for the page to load (use `browser_wait_for` with a known selector like `h1`).
111
- 3. **Inject a navigation guard** using `browser_evaluate` to prevent link clicks from
112
- navigating away while still allowing the app's own click handlers (including
113
- tracking) to run. Use a **bubble-phase** listener so the app's capture-phase and
114
- bubble-phase handlers fire first:
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):
115
188
  ```js
116
- () => {
117
- document.addEventListener('click', (e) => {
118
- const a = e.target.closest('a');
119
- if (a) e.preventDefault();
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
+ }
120
202
  });
121
203
  }
122
204
  ```
123
- This approach ensures tracking events fire before navigation is cancelled.
124
- **Do NOT add a `beforeunload` listener** — it causes a "Leave site?" browser dialog
125
- that blocks every subsequent Playwright navigation.
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.
126
209
 
127
210
  4. Take a `browser_snapshot` to get the current accessibility tree. Use the refs from
128
211
  this snapshot for all subsequent clicks on this page.
@@ -130,17 +213,48 @@ For **each page URL** discovered in Step 2:
130
213
  5. For **each matched element** (visible, not disabled):
131
214
  a. Click it using `browser_click`.
132
215
  b. Wait 500ms for async effects.
133
- c. If the click opened a modal or significantly changed the DOM, take a new
134
- `browser_snapshot` before proceeding — snapshot refs become stale after DOM
135
- changes and will cause errors if reused.
136
- d. If a dialog appears (check the snapshot for `Modal state`), dismiss it with
137
- `browser_handle_dialog` before continuing.
138
-
139
- 6. After all clicks on the page, call `browser_console_messages` **once** with
140
- `level: "debug"`. Since `all` defaults to `false`, this returns only the messages
141
- from the current page navigation — no need to diff between clicks.
142
-
143
- 7. Store all messages with the page route (e.g. `/home/`) as their source.
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.
144
258
 
145
259
  ---
146
260
 
@@ -180,13 +294,30 @@ Gather every console message captured across all pages into a flat list:
180
294
 
181
295
  #### Deduplicate
182
296
 
183
- Two messages are duplicates if they share the same `text`. When the same message
184
- appears on multiple pages, record all pages it appeared on.
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`.
185
316
 
186
317
  #### Tiers
187
318
 
188
319
  Organize into four tiers:
189
- 1. **Tracking Events** — messages matching the `DEBUG: track` pattern
320
+ 1. **Tracking Events** — messages matching the `DEBUG: track` pattern (split into Global and Slide sub-sections, see Step 5)
190
321
  2. **Errors** — `console.error`
191
322
  3. **Warnings** — `console.warn`
192
323
  4. **Info / Log** — `console.log`, `console.info`, `console.debug` (excluding tracking events)
@@ -195,22 +326,44 @@ Organize into four tiers:
195
326
 
196
327
  ### Step 5 — Write the report
197
328
 
198
- Write a Markdown file to `analytics-audit-report.md` in the project root with this structure:
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:
199
334
 
200
335
  ```markdown
201
336
  # Browser Console Log Audit
202
337
 
203
338
  **Date:** <today's date>
204
339
  **Pages audited:** /home/, /about/, /about/moa/
205
- **Total unique messages:** <N>
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
206
343
 
207
344
  ---
208
345
 
209
346
  ## Tracking Events (<count>)
210
347
 
211
- | Slide | Type | Name | Pages |
212
- |-------|------|------|-------|
213
- | `home` | `button` | `button_evo_flip_card_trigger` | /home/ |
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` |
214
367
 
215
368
  ## Errors (<count>)
216
369
 
@@ -235,10 +388,15 @@ Write a Markdown file to `analytics-audit-report.md` in the project root with th
235
388
  *Generated by the analytics-auditor skill.*
236
389
  ```
237
390
 
238
- If a tier has zero messages, write `_None_` instead of a table.
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.
239
395
 
240
- After writing the file, tell the user where the report was saved and give a brief
241
- summary (e.g. "Found 8 tracking events, 2 errors, 1 warning, 5 info messages across 3 pages").
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").
242
400
 
243
401
  ---
244
402
 
@@ -263,11 +421,20 @@ Do **not** stop the dev server — the user likely wants to keep it running.
263
421
  - **`grep -P` is not supported on Windows** — use `grep -o` with basic regex patterns.
264
422
  - **Snapshot refs go stale** after DOM mutations (modal open/close, tab switches). Always
265
423
  re-snapshot after any click that visibly changes the page structure.
266
- - **`browser_console_messages` fails if a dialog is showing** — check the snapshot for
267
- `Modal state` and dismiss with `browser_handle_dialog` before calling it.
268
- - **Call `browser_console_messages` once per page** (at the end), not after every click.
269
- The default `all: false` scopes results to the current navigation, giving a clean
270
- per-page log with no diffing needed.
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.
271
438
  - If the user provides their own list of selectors or pages, replace the defaults in
272
439
  Step 2 before proceeding.
273
440
  - If a selector does not match any element on a page, log a warning in the report's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inizioevoke/veeva-astroclm-core",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "",