@bridgeline-digital/hawk-app-cli 0.1.1-dev.20261026155507 → 0.1.1-dev.20261026224639
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.
- package/README.md +53 -2
- package/agents/AGENTS.md +21 -0
- package/claude-plugin/hawk-apps/skills/add-query/SKILL.md +3 -1
- package/claude-plugin/hawk-apps/skills/new-screen/SKILL.md +3 -0
- package/docs/bridge/README.md +93 -8
- package/docs/ui/README.md +12 -3
- package/package.json +1 -1
- package/schema/hawk-app.schema.json +2 -1
- package/src/cli.mjs +44 -4
- package/src/data.mjs +61 -13
- package/src/dev.mjs +146 -14
- package/src/manifest.mjs +17 -1
- package/src/mcp.mjs +55 -5
- package/src/platform.mjs +12 -0
- package/src/problems.mjs +48 -0
- package/src/replay.mjs +176 -0
- package/src/scope-check.mjs +1 -0
- package/src/shape.mjs +111 -0
- package/src/telemetry.mjs +122 -2
package/README.md
CHANGED
|
@@ -306,14 +306,22 @@ HawkSearch). The full picture, with diagrams: docs/versions-and-aliases.md in th
|
|
|
306
306
|
|
|
307
307
|
## Errors and usage (`hawk-app errors`, `hawk-app usage`)
|
|
308
308
|
|
|
309
|
-
The bridge reports the app's page errors
|
|
309
|
+
The bridge reports the app's page errors, one open per page load, and the screens and actions used
|
|
310
|
+
(platform/docs/telemetry.md):
|
|
310
311
|
|
|
311
312
|
```bash
|
|
312
313
|
npx hawk-app errors # the last 7 days (--days 1-30): grouped, the most frequent first, with versions, clients and an [id]
|
|
313
314
|
npx hawk-app errors --explain 3fa1c2d9 # one error's brief, then Claude's likely cause and patch (--no-ai: the brief only)
|
|
314
315
|
npx hawk-app usage # the last 30 days (--days 1-400): opens, view opens, users and clients; staff apart
|
|
316
|
+
npx hawk-app usage --screens # screens and actions: the most used, then the least used, with the 3 newest versions
|
|
317
|
+
npx hawk-app usage --screens --version 4 # one version's counts
|
|
318
|
+
npx hawk-app usage --speed # each screen's data time (median, 90%) beside the version before, slower ones flagged; the slowest calls
|
|
315
319
|
```
|
|
316
320
|
|
|
321
|
+
**What people use.** Screens are the app's routes (ids as `:id`), its views, and what it names with `app.usage.screen`
|
|
322
|
+
(the UI kit's `useScreen('Logs')`, for a tab); actions are clicks on `data-hawk-usage="Export CSV"` elements and
|
|
323
|
+
`app.usage.action`. Compare versions to see whether a change is used, and the least used to see what to simplify.
|
|
324
|
+
|
|
317
325
|
**Errors that come with a fix.** `--explain <id>` writes the error's **brief** and asks Claude for the cause:
|
|
318
326
|
|
|
319
327
|
- **The stack, mapped to your source:** each of the app's frames as `src/app/page.tsx:42:17`, with the code around it
|
|
@@ -333,6 +341,38 @@ It needs the version's **source maps**: the templates build them (`productionBro
|
|
|
333
341
|
platform's private bucket instead of the CDN, so your source is never public. Without maps the stack stays minified and
|
|
334
342
|
the brief says so. Errors of versions deployed before keep their minified stacks; the next deploy maps the new ones.
|
|
335
343
|
|
|
344
|
+
## Problems users report (`hawk-app problems`)
|
|
345
|
+
|
|
346
|
+
Users report what went wrong with the flag icon in the app's bar in HawkSearch: their words, a picture of the page,
|
|
347
|
+
what the page showed, the version, and a recording if they agreed to one (platform/docs/problems.md).
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
npx hawk-app problems # the newest first: id, when, client, version, page, what came with it
|
|
351
|
+
npx hawk-app problems show pk7m2n9x4q # the words, a 5-minute link to the picture, the page context, the recording
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
The client's data: Bridgeline users only (`hawk-app sso`), kept 30 days, nothing written to disk. Declare and set
|
|
355
|
+
`reportEmails` (`{ "type": "emails" }`) to get each report by email.
|
|
356
|
+
|
|
357
|
+
## Replay a client's problem (`hawk-app dev --replay`)
|
|
358
|
+
|
|
359
|
+
"It shows nothing for client X": a Bridgeline user opens the app as X in HawkSearch and turns on **Record this
|
|
360
|
+
session** (the review bar's ⋮ menu). For up to 30 minutes the platform keeps every call the app makes in their session,
|
|
361
|
+
with its answer (platform/docs/recordings.md). Then:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
npx hawk-app recordings # the app's recorded sessions (kept 7 days): id, who, client, calls, failed calls
|
|
365
|
+
npx hawk-app dev --replay r7k2m9xq4p # your local code, answered as it was for that client
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
- `dev --replay` runs as the recorded client and site. Each call gets the recorded answer (the same method, path and
|
|
369
|
+
body; else the same path; else the same route with other query values), so the page behaves as it did, and you fix
|
|
370
|
+
it against the real case. A call the session never made answers 501 `not_recorded` (the terminal lists them); the
|
|
371
|
+
AI stream isn't recorded. The errors the page reported then are listed at start.
|
|
372
|
+
- Nothing reaches the platform, and the session's date range is used.
|
|
373
|
+
- **The client's data:** Bridgeline users only (`hawk-app sso` keys; others get 403). The recording stays in memory
|
|
374
|
+
(never on disk), is gone when you stop, and is never given to an AI assistant. Don't share screenshots of it.
|
|
375
|
+
|
|
336
376
|
## Accessibility check
|
|
337
377
|
|
|
338
378
|
`hawk-app deploy` runs axe-core (WCAG 2.1 A/AA and best practices) over up to 10 built pages and sends the result with
|
|
@@ -562,6 +602,17 @@ up to 50 rows plus the bytes scanned. `--client` is the HawkSearch client id (th
|
|
|
562
602
|
`context.tenant.clientId`); on dev, use a demo or test client. `string[]` values are comma-separated (`--param engines=a,b`). It only works
|
|
563
603
|
against the dev platform. When too many queries are running (429 `too_many_queries`) it waits and tries again.
|
|
564
604
|
|
|
605
|
+
```bash
|
|
606
|
+
npx hawk-app data shape queries/top-searches.sql --param from=2026-09-01 --param to=2026-09-30
|
|
607
|
+
npx hawk-app data shape report:top-searches --param engine=12
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
`data shape` runs a query or an analytics report for your dev client (`HAWK_APPS_DEV_CLIENT`, or `--client`) and
|
|
611
|
+
prints only its **shape**: each column's type, what its values look like (number text, dates, uuids, urls), how often
|
|
612
|
+
it's empty, how many distinct values and its size, all bucketed. Never a value. It's what the MCP server's `data_shape`
|
|
613
|
+
tool gives Claude Code, so the code it writes handles the real answer (numbers sent as text, empty columns) without
|
|
614
|
+
client data entering the chat.
|
|
615
|
+
|
|
565
616
|
The sensitive tables (visitor-level data such as `summary`) are listed apart: only apps Engineering has opted in can
|
|
566
617
|
query them. Ask Engineering if your app needs one.
|
|
567
618
|
|
|
@@ -739,7 +790,7 @@ attribute (`"data": "hawk-keyword"`), used as is when the page has it, with its
|
|
|
739
790
|
(the bridge README's **Actions**, with every target setting). `hawk-app validate` checks the targets (paths and
|
|
740
791
|
patterns are regular expressions, params the view declares); `hawk-app status` lists the actions. `hawk-app dev` has an
|
|
741
792
|
**Action** menu: pick an action, type a value, and the view opens in a dialog as in HawkSearch. For complete recipes
|
|
742
|
-
(actions, schedules, events, a client API endpoint and their tests), see [
|
|
793
|
+
(actions, schedules, events, a client API endpoint and their tests), see the [Recipes](../../docs/examples.md).
|
|
743
794
|
|
|
744
795
|
### Testing functions (`hawk-app test`)
|
|
745
796
|
|
package/agents/AGENTS.md
CHANGED
|
@@ -146,10 +146,26 @@ With the UI kit: `usePageContext(rows ? { page: '<the page>', dateRange, totals,
|
|
|
146
146
|
`app.assistant.setContext({ ... })` whenever the data or filters change. Plain names and numbers of what's on screen,
|
|
147
147
|
at most 50 KB; summarize long tables. `hawk-app validate` warns when an app never sets one.
|
|
148
148
|
|
|
149
|
+
**Comments** (scope `comments`): `<HawkComments target="keyword:…" label="…" />` from the UI kit on what people discuss
|
|
150
|
+
(a row, a widget, `page:summary`); targets are stable ids, never personal data. Declare `config.commentEmails`
|
|
151
|
+
(`{ "type": "emails" }`) so new comments are emailed (`hawk-app config set commentEmails …`, a person or a Slack
|
|
152
|
+
channel's email).
|
|
153
|
+
|
|
149
154
|
**Export as PDF** (only on pages worth keeping: a report, a summary): `usePdfExport({ fileName })` from the UI kit
|
|
150
155
|
(`false` while loading), or `app.setPdfExport({ fileName })` and `app.setPdfExport(null)` when the page leaves. The
|
|
151
156
|
host shows a PDF icon and downloads the whole page; mark buttons and filters `data-hawk-pdf-ignore`.
|
|
152
157
|
|
|
158
|
+
**Saved views:** keep what makes a page's state (filters, range, tab, sort) in the UI kit's `useSavedState(initial)`
|
|
159
|
+
(or `app.state.set` and `app.state.onApply`), so users can save, share and link views from HawkSearch's Views menu. One
|
|
160
|
+
small JSON object, never the data.
|
|
161
|
+
|
|
162
|
+
**Ratings and reviews** need no code: users rate the app from HawkSearch's app header. To get each new review by email,
|
|
163
|
+
declare `config.reviewEmails` (`{ "type": "emails" }`) and set it (`hawk-app config set reviewEmails …`).
|
|
164
|
+
|
|
165
|
+
**Usage** is counted by the bridge: routes and views as screens. Name what they don't show: `useScreen('Logs')` from the
|
|
166
|
+
UI kit in each tab (or `app.usage.screen`), and mark the main buttons `data-hawk-usage="Export CSV"` (or
|
|
167
|
+
`app.usage.action`). Name the thing, never data (a keyword, a client).
|
|
168
|
+
|
|
153
169
|
**Actions** (`actions` in `hawk-app.json`) add a button to HawkSearch's own pages that opens a view in a dialog:
|
|
154
170
|
`{ "<name>": { "label": "Fix with …", "view": "fix", "targets": { "hawksearch": [{ "path": "<regex on the page path>",
|
|
155
171
|
"selector": "<CSS: each thing>", "anchor"?: "<CSS inside it>", "position"?: "after", "params": { "keyword": { "data"?,
|
|
@@ -195,6 +211,11 @@ npx hawk-app config list # config values (config set <key> <value> [--cl
|
|
|
195
211
|
npx hawk-app errors # errors in the app's pages (deployed versions), the most frequent first, with [id]s
|
|
196
212
|
npx hawk-app errors --explain <id> # one error mapped to the source, the failed calls, what changed; then a likely fix
|
|
197
213
|
npx hawk-app usage # opens, users and clients per day
|
|
214
|
+
npx hawk-app usage --screens # the screens and actions used, most and least, per version (--version <n>)
|
|
215
|
+
npx hawk-app usage --speed # how fast each screen gets its data, per version; the slowest platform calls
|
|
216
|
+
npx hawk-app data shape report:<name> # what a report (or queries/<name>.sql) really answers: types, empties, sizes; no values
|
|
217
|
+
npx hawk-app problems # what users reported ("Report a problem"); hawk-app problems show <id> in a terminal (client data)
|
|
218
|
+
npx hawk-app recordings # recorded sessions of a client's problem; the developer replays one: hawk-app dev --replay <id>
|
|
198
219
|
npx hawk-app upgrade # bring the app (and this file) up to the CLI's current template
|
|
199
220
|
npx hawk-app upgrade --channel latest # a dev app (bridge/CLI on the `dev` tag) to the released versions
|
|
200
221
|
```
|
|
@@ -25,7 +25,9 @@ each version and approvers review them. Check first whether an analytics report
|
|
|
25
25
|
`min`/`max`, `maxItems`.
|
|
26
26
|
5. **Call it** with `app.data.query('<name>', { from, to })` (or `ctx.platform.data.query` in a function). Look up the
|
|
27
27
|
result shape and errors with `docs` ("data.query"). Handle a client without analytics.
|
|
28
|
-
6. **Check it.** Run `validate
|
|
28
|
+
6. **Check it.** Run `validate`, then `data_shape` with `queries/<name>.sql` and its params: it runs the query for the
|
|
29
|
+
developer's dev client and gives you its shape (types, empty rates, sizes), never rows. Adjust the SQL (casts,
|
|
30
|
+
`COALESCE`) and the code reading it to that shape. To see the rows themselves, tell the user to run
|
|
29
31
|
`npx hawk-app data run queries/<name>.sql --client <test client id> --param from=… --param to=…` in a terminal: its rows
|
|
30
32
|
are client data and must not come into the chat. Queries run only from a **deployed** version, so the app sees a
|
|
31
33
|
new or changed query after a deploy (only when the user asks).
|
|
@@ -19,6 +19,9 @@ colors and light/dark mode. Never restyle the kit or add another font.
|
|
|
19
19
|
- the client's HawkSearch setup or searches: `hawksearch_api`;
|
|
20
20
|
- your own SQL: the **add-query** skill;
|
|
21
21
|
- server work or secrets: the **add-function** skill.
|
|
22
|
+
Then call `data_shape` for each report or query the screen reads (`report:<name>` with its fields, or
|
|
23
|
+
`queries/<name>.sql`): it shows what really comes back for the developer's client (number-as-text, empty columns,
|
|
24
|
+
nulls, sizes), never the values. Write the parsing, empty states and formatting for that shape.
|
|
22
25
|
Tell the user which scopes the screen needs before adding them to `hawk-app.json`.
|
|
23
26
|
4. **Build it** in the app's style:
|
|
24
27
|
- read the client, engine and dates from `app.context` and re-load on `app.onContextChange`;
|
package/docs/bridge/README.md
CHANGED
|
@@ -150,13 +150,15 @@ buttons appear in HawkSearch: nothing changes in HawkSearch for it.
|
|
|
150
150
|
"params": { "keyword": { "selector": "td.table__cell:first-child" } } // its text
|
|
151
151
|
},
|
|
152
152
|
{
|
|
153
|
+
// AI Advisor: only the insights that suggest a synonym or a redirect (HawkSearch marks each insight with
|
|
154
|
+
// data-hawk-action, data-hawk-category, data-hawk-keyword and data-hawk-insight-id).
|
|
153
155
|
"path": "/admin/recommendations/?$",
|
|
154
|
-
"selector": ".recommendations-list__item
|
|
156
|
+
"selector": ".recommendations-list__item[data-hawk-action=\"Synonym\"], .recommendations-list__item[data-hawk-action=\"Redirect\"]",
|
|
155
157
|
"anchor": ".recommendations-list__text",
|
|
156
158
|
"position": "after",
|
|
157
159
|
"params": {
|
|
158
|
-
"keyword": { "
|
|
159
|
-
"category": { "
|
|
160
|
+
"keyword": { "data": "hawk-keyword" }, // the insight's exact term
|
|
161
|
+
"category": { "data": "hawk-category" }
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
164
|
]
|
|
@@ -177,7 +179,8 @@ buttons appear in HawkSearch: nothing changes in HawkSearch for it.
|
|
|
177
179
|
- **Pick stable selectors:** ids, `data-*` attributes and element names over styling classes. HawkSearch doesn't know
|
|
178
180
|
about your selectors: when it changes its markup, your buttons stop appearing until you deploy new targets.
|
|
179
181
|
- **Approval:** `actions` is a setting that always needs an approver (never auto-approve): reviewers see each target and
|
|
180
|
-
what it reads from the page.
|
|
182
|
+
what it reads from the page. Before approval, Bridgeline staff already get the new actions on a draft app (the version
|
|
183
|
+
they open), so they can try the buttons in HawkSearch first; clients only ever get approved ones. Read only what the view needs. Settings and the Apps pages never show actions.
|
|
181
184
|
|
|
182
185
|
```tsx
|
|
183
186
|
// src/app/views/fix/page.tsx
|
|
@@ -1209,6 +1212,46 @@ await app.storage.files.delete('exports/prices.csv');
|
|
|
1209
1212
|
|
|
1210
1213
|
See the platform's docs/storage.md.
|
|
1211
1214
|
|
|
1215
|
+
## Comments: `comments`
|
|
1216
|
+
|
|
1217
|
+
Let the people using the app talk about what it shows, right where it is: a comment thread on a search term, a report
|
|
1218
|
+
row, a widget or the whole page. Everyone at the client who uses the app sees the comments, and so do Bridgeline staff.
|
|
1219
|
+
They're kept until deleted, and each new one is emailed to whoever you set.
|
|
1220
|
+
|
|
1221
|
+
```json
|
|
1222
|
+
{
|
|
1223
|
+
"scopes": ["comments"],
|
|
1224
|
+
"config": { "commentEmails": { "type": "emails", "label": "Who gets new comments" } }
|
|
1225
|
+
}
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
```tsx
|
|
1229
|
+
// The UI kit: a comment icon with a "!" when there are comments (one count request for all the page's icons); a click opens the thread.
|
|
1230
|
+
<HawkComments target={`keyword:${row.keyword}`} label={row.keyword} />
|
|
1231
|
+
<HawkComments target="page:summary" label="This month's summary" />
|
|
1232
|
+
```
|
|
1233
|
+
|
|
1234
|
+
```ts
|
|
1235
|
+
// Or the bridge itself:
|
|
1236
|
+
const thread = await app.comments.list('keyword:rain-boots'); // [{ id, text, author, createdAt, mine, canDelete, … }], oldest first
|
|
1237
|
+
await app.comments.add('keyword:rain-boots', 'Catalog gap or synonym?', { label: 'rain boots' });
|
|
1238
|
+
await app.comments.edit('keyword:rain-boots', id, 'Synonym: we sell rain boots.'); // the author only
|
|
1239
|
+
await app.comments.delete('keyword:rain-boots', id); // the author, or Bridgeline staff
|
|
1240
|
+
const counts = await app.comments.counts(['keyword:rain-boots', 'keyword:wellies']); // { 'keyword:rain-boots': 2 }
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
- **Targets** are yours: stable ids of 1-200 characters (letters, digits, `. _ - : / # @`), never personal data. The
|
|
1244
|
+
same target on two pages is the same thread. `label` names it in words for the drawer and the email.
|
|
1245
|
+
- **Email:** set `commentEmails` (declared as `emails` in `config`) to a person, a team list or a Slack channel's
|
|
1246
|
+
email address (Slack: the channel's Integrations › Send emails to this channel), for every client or one:
|
|
1247
|
+
`npx hawk-app config set commentEmails merch@agency.example [--client e3141]`. No deploy needed. Each new comment is
|
|
1248
|
+
sent there from the platform, with the app's name, the target and the client; replies go to the author. Without the
|
|
1249
|
+
key, nothing is sent. `hawk-app validate` warns about it.
|
|
1250
|
+
- **Authors** are shown by the email the host sends; Bridgeline staff are marked. Text only, up to 4,000 characters;
|
|
1251
|
+
30 new or edited comments a minute per app and client (429 `too_many_requests`).
|
|
1252
|
+
- Errors (`HawkApiError`): 400 `invalid_target` / `invalid_comment`, 403 `scope_missing` / `not_author`, 404
|
|
1253
|
+
`comment_not_found`. See the platform's docs/comments.md.
|
|
1254
|
+
|
|
1212
1255
|
## External Lambdas: `lambdas:invoke`
|
|
1213
1256
|
|
|
1214
1257
|
Call a Lambda written outside Hawk Apps (for example HawkAnalytics' client setup) from the app or a function. Declare
|
|
@@ -1409,11 +1452,31 @@ Failed data calls throw `HawkApiError` with `status` and `code`; failed host req
|
|
|
1409
1452
|
|
|
1410
1453
|
A `403` without one of these codes means the app lacks the scope for that call.
|
|
1411
1454
|
|
|
1455
|
+
## Saved views
|
|
1456
|
+
|
|
1457
|
+
Users save the page's state under a name and open it again later or share it with their company, from the host's
|
|
1458
|
+
Views menu (a bookmark icon) or a link. Say what makes the page's state, and restore it:
|
|
1459
|
+
|
|
1460
|
+
```ts
|
|
1461
|
+
app.state.set({ range, target, sort }); // as it changes; null while there's nothing to save
|
|
1462
|
+
const stop = app.state.onApply((state, view) => { // a saved view (or its link) was opened
|
|
1463
|
+
setRange(state.range);
|
|
1464
|
+
setTarget(state.target);
|
|
1465
|
+
setSort(state.sort);
|
|
1466
|
+
});
|
|
1467
|
+
```
|
|
1468
|
+
|
|
1469
|
+
One small JSON object (at most 16 KB), never the page's data. The menu shows only while the app has set a state; a view
|
|
1470
|
+
opened by a link reaches `onApply` even if the app registers it later. The UI kit's `useSavedState(initial)` does both
|
|
1471
|
+
(`const [filters, setFilters] = useSavedState({ range: 'l13', target: null })`). Errors: `invalid_request` (not a JSON
|
|
1472
|
+
object, too large). See the platform's docs/saved-views.md.
|
|
1473
|
+
|
|
1412
1474
|
## Errors and usage
|
|
1413
1475
|
|
|
1414
|
-
`createApp` reports, on its own, one open per page load (the full app or the view showing)
|
|
1415
|
-
errors and unhandled rejections: at most 10 per page load, each once, without query
|
|
1416
|
-
with `hawk-app errors` and `hawk-app usage`, and Bridgeline in the portal
|
|
1476
|
+
`createApp` reports, on its own, one open per page load (the full app or the view showing), the screens and actions
|
|
1477
|
+
used, and the page's uncaught errors and unhandled rejections: at most 10 per page load, each once, without query
|
|
1478
|
+
strings. The app's team sees them with `hawk-app errors` and `hawk-app usage`, and Bridgeline in the portal
|
|
1479
|
+
(platform/docs/telemetry.md).
|
|
1417
1480
|
|
|
1418
1481
|
```ts
|
|
1419
1482
|
try {
|
|
@@ -1424,7 +1487,26 @@ try {
|
|
|
1424
1487
|
}
|
|
1425
1488
|
```
|
|
1426
1489
|
|
|
1427
|
-
|
|
1490
|
+
**What people use.** The bridge also counts the **screens** people open (each route of the full app, as `/orders/:id`
|
|
1491
|
+
with ids left out, or the view showing) and the **actions** they take: clicks on elements marked `data-hawk-usage`. Only
|
|
1492
|
+
these names are sent, batched every 15 s, never what users type or see. Count what the routes don't show yourself:
|
|
1493
|
+
|
|
1494
|
+
```tsx
|
|
1495
|
+
<Button data-hawk-usage="Export CSV" onClick={exportCsv}>Export</Button> // counted on each click
|
|
1496
|
+
|
|
1497
|
+
app.usage.screen('Logs'); // a tab or step within one route (the UI kit: useScreen('Logs') in the tab)
|
|
1498
|
+
app.usage.action('Bulk fix'); // an action that isn't a click on one element
|
|
1499
|
+
```
|
|
1500
|
+
|
|
1501
|
+
Names are letters, digits and simple punctuation, up to 80 characters: name the thing (`Export CSV`), not the data
|
|
1502
|
+
(never a keyword or a client). `hawk-app usage --screens` lists the most and least used, per version.
|
|
1503
|
+
|
|
1504
|
+
**How fast it is.** The bridge also times each screen: how long the page took to load, how long a screen took to get
|
|
1505
|
+
its data (until its platform calls are done), slow clicks (40 ms or more to the next paint) and each platform call by
|
|
1506
|
+
route. Only times and names. `hawk-app usage --speed` shows each screen's median and 90th percentile, the version
|
|
1507
|
+
beside the one before it, and the slowest calls.
|
|
1508
|
+
|
|
1509
|
+
`createApp({ reportErrors: false, reportOpens: false, reportUsage: false, reportSpeed: false })` turns any of them off.
|
|
1428
1510
|
|
|
1429
1511
|
## Talk to the host
|
|
1430
1512
|
|
|
@@ -1522,6 +1604,9 @@ useEffect(() => void app.assistant.setContext({ page: 'Recommendation Dashboard'
|
|
|
1522
1604
|
await app.assistant.ask('Which widgets are taking up space?');
|
|
1523
1605
|
```
|
|
1524
1606
|
|
|
1607
|
+
Asking from a dialog or drawer is fine: the host's assistant is part of the page the dialog covers, so the host closes
|
|
1608
|
+
the dialog first (as a click on its backdrop would) and the answer is in view.
|
|
1609
|
+
|
|
1525
1610
|
A message is at most 2,000 characters. Errors (`HawkBridgeError`): `unsupported` (no assistant on the page), `scope_missing`,
|
|
1526
1611
|
`invalid_request`; `setContext` needs no scope (without it, only "Explain this page" gets the context). Hosts: `<hawk-app-frame>` fires a cancelable `hawk-assistant` event (`detail.message`; `hawk-assistant-context` with `detail.context` for the context); a host that
|
|
1527
1612
|
answers it itself calls `preventDefault()` and sets the element's `assistant` attribute. Otherwise the element sends the
|
package/docs/ui/README.md
CHANGED
|
@@ -42,9 +42,19 @@ export function Page() {
|
|
|
42
42
|
then renders its children in the theme. It follows the host when its theme changes.
|
|
43
43
|
- **`useHawk()`** returns `{ app, context }`, always connected (the provider renders children only then).
|
|
44
44
|
`useHawkAppState()` is the hook underneath, for an app that wants to render the states itself.
|
|
45
|
+
- **`<HawkComments target label?>`** (the `comments` scope) is a comment icon with a "!" when there are comments (the count in its tooltip); a click opens the
|
|
46
|
+
thread in a drawer, where everyone at the client reads and adds comments (authors edit and delete their own). The
|
|
47
|
+
page's badges share one count request. See the bridge guide's "Comments".
|
|
45
48
|
- **`usePdfExport(options | false)`** lets the user export the page as a PDF while it's mounted: the host shows a PDF
|
|
46
49
|
icon, and a click downloads the whole page. `usePdfExport({ fileName: 'Zero results' })`, or `false` while loading.
|
|
47
50
|
Leave out buttons and filters with `data-hawk-pdf-ignore`. See the bridge guide's "Export a page as a PDF".
|
|
51
|
+
- **`useSavedState(initial)`** is `useState` for what makes a saved view (filters, range, tab, sort): the value is the
|
|
52
|
+
page's state for the host's Views menu, and opening a saved view or its link sets it back.
|
|
53
|
+
`const [filters, setFilters] = useSavedState({ range: 'l13', target: null })`. One small JSON object, once per page.
|
|
54
|
+
See the bridge guide's "Saved views".
|
|
55
|
+
- **`useScreen(name | null)`** counts a screen the routes don't show, such as a tab, each time it's shown:
|
|
56
|
+
`useScreen('Logs')` in the tab's component. Routes and views are counted by the bridge, and so are clicks on
|
|
57
|
+
`data-hawk-usage="Export CSV"` elements. See `hawk-app usage --screens` and the bridge guide's "Errors and usage".
|
|
48
58
|
- **`usePageContext(object | null)`** says what the page shows, for the host's **Explain this page** button (and its
|
|
49
59
|
assistant): `usePageContext(rows ? { page: 'Zero-result searches', dateRange, top10 } : null)`. Plain names and
|
|
50
60
|
numbers, at most 50 KB; sent shortly after it changes, cleared when the component unmounts. See the bridge guide's
|
|
@@ -85,9 +95,8 @@ const f = useFormat(); // in the host's locale
|
|
|
85
95
|
|
|
86
96
|
The host shows the frame over the whole page while a dialog or drawer is open, so they're always fully visible.
|
|
87
97
|
|
|
88
|
-
Every component, in light and dark, is in the [UI kit gallery](GALLERY.md) on the developer site (the
|
|
89
|
-
|
|
90
|
-
see it in a real one). `<HawkAppProvider preview={{ mode: 'dark' }}>` renders without a host, with sample context
|
|
98
|
+
Every component, in light and dark, is in the [UI kit gallery](GALLERY.md) on the developer site (the repository's
|
|
99
|
+
gallery app, `examples/ui-kit`, rendered as a preview). `<HawkAppProvider preview={{ mode: 'dark' }}>` renders without a host, with sample context
|
|
91
100
|
(`previewContext()`), for galleries, docs and screenshots. New apps get the kit from the CLI's `next-mui` template
|
|
92
101
|
(`hawk-app create my-app --template next-mui`); an existing next-mui app gets it with `hawk-app upgrade`.
|
|
93
102
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridgeline-digital/hawk-app-cli",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.20261026224639",
|
|
4
4
|
"description": "For Bridgeline developers: create, deploy and register Hawk Apps (Services-built tools embedded in HawkSearch). Requires a Hawk Apps publisher key.",
|
|
5
5
|
"author": "Bridgeline Digital",
|
|
6
6
|
"license": "UNLICENSED",
|
package/src/cli.mjs
CHANGED
|
@@ -30,6 +30,8 @@ import { testCommand } from './test-command.mjs';
|
|
|
30
30
|
import { appChannel } from './channel.mjs';
|
|
31
31
|
import { accessibilityWarnings, checkAccessibility } from './accessibility.mjs';
|
|
32
32
|
import { errorsCommand, usageCommand } from './telemetry.mjs';
|
|
33
|
+
import { recordingsCommand } from './replay.mjs';
|
|
34
|
+
import { problemsCommand } from './problems.mjs';
|
|
33
35
|
|
|
34
36
|
/** The short help: what each command is for, by task. `hawk-app help <command>` shows a command's details. */
|
|
35
37
|
const HELP = `hawk-app: build, run and release Hawk Apps
|
|
@@ -65,7 +67,9 @@ Data and functions
|
|
|
65
67
|
secrets, logs Function secrets and logs
|
|
66
68
|
|
|
67
69
|
Monitor
|
|
68
|
-
errors, usage The app's page errors
|
|
70
|
+
errors, usage The app's page errors; its opens, users, clients, screens and actions
|
|
71
|
+
recordings Recorded sessions to replay with hawk-app dev --replay <id> (Bridgeline users)
|
|
72
|
+
problems What users reported with "Report a problem" (Bridgeline users)
|
|
69
73
|
|
|
70
74
|
Account
|
|
71
75
|
login, whoami, logout A publisher key (for CI), and which key and platform apply here
|
|
@@ -164,6 +168,20 @@ Usage:
|
|
|
164
168
|
from the deployed version: deploy after changing them. The
|
|
165
169
|
client API ("api") runs the local functions/ instead:
|
|
166
170
|
/v1/clients/<id>/apps/<slug>/api/<name> on this port
|
|
171
|
+
hawk-app dev --replay <id> [--url <app dev URL>] [--port 3200] [--no-start]
|
|
172
|
+
Run your local code against a recorded session (a Bridgeline
|
|
173
|
+
user's "Record this session" in HawkSearch): the app's calls are
|
|
174
|
+
answered as they were for that client, nothing reaches the
|
|
175
|
+
platform. Bridgeline users only (hawk-app sso); kept in memory
|
|
176
|
+
hawk-app usage --speed [--days 14] [--version <n>]
|
|
177
|
+
How fast each screen gets its data (median and 90th percentile),
|
|
178
|
+
the version beside the one before it (slower ones flagged), the
|
|
179
|
+
page's load, slow clicks and the slowest platform calls
|
|
180
|
+
hawk-app recordings The app's recorded sessions (kept 7 days), with their ids
|
|
181
|
+
hawk-app problems [show <id>] What users reported with "Report a problem" (the flag in the
|
|
182
|
+
app's bar): their words, a picture of the page (a 5-minute link),
|
|
183
|
+
what the page showed, the version, a recording if they agreed.
|
|
184
|
+
Bridgeline users only; kept 30 days
|
|
167
185
|
|
|
168
186
|
Clients (who has the app; also managed in the Hawk Analytics portal, not in hawk-app.json):
|
|
169
187
|
hawk-app clients Show the app's visibility and its clients
|
|
@@ -196,6 +214,11 @@ Monitor:
|
|
|
196
214
|
cause and patch (--no-ai: the brief only)
|
|
197
215
|
hawk-app usage [--days 30] Opens of the app and its views, users and clients per day (up to
|
|
198
216
|
400 days; Bridgeline staff counted apart)
|
|
217
|
+
hawk-app usage --screens [--days 30] [--version <n>]
|
|
218
|
+
The screens (routes, views, app.usage.screen) and actions
|
|
219
|
+
([data-hawk-usage], app.usage.action) people use: the most used
|
|
220
|
+
first, the least used last, the newest versions side by side
|
|
221
|
+
(--version: one version's counts)
|
|
199
222
|
|
|
200
223
|
Custom data (app queries, scope data:read):
|
|
201
224
|
hawk-app data tables [--app <slug>] List the tables queries can use (--app: is the app
|
|
@@ -204,6 +227,11 @@ Custom data (app queries, scope data:read):
|
|
|
204
227
|
hawk-app data run <queries/name.sql> --client <id> [--param name=value …]
|
|
205
228
|
Run a query against a client's data (dev platform only);
|
|
206
229
|
string[] values are comma-separated
|
|
230
|
+
hawk-app data shape <queries/name.sql | report:<name>> [--client <id>] [--param name=value …]
|
|
231
|
+
What a query or analytics report answers, as its shape only
|
|
232
|
+
(columns, types, how often empty, what values look like,
|
|
233
|
+
bucketed sizes): never the values. Default client: your dev
|
|
234
|
+
client (HAWK_APPS_DEV_CLIENT). For the AI that writes the code
|
|
207
235
|
|
|
208
236
|
Functions (server-side code, scope functions:invoke):
|
|
209
237
|
hawk-app secrets list List the app's secrets (names only, never values)
|
|
@@ -600,7 +628,8 @@ export async function main(argv, { fetchImpl = fetch, log = console.log, runBuil
|
|
|
600
628
|
case 'dev': {
|
|
601
629
|
const manifest = await loadManifest(manifestFile);
|
|
602
630
|
const projectDir = dirname(resolve(manifestFile));
|
|
603
|
-
|
|
631
|
+
// A replay runs as the recorded client.
|
|
632
|
+
if (!flags.replay && !flags.client && !(env ?? process.env).HAWK_APPS_DEV_CLIENT) {
|
|
604
633
|
// No client yet: ask which one (by name), and remember it in .env.local for next time.
|
|
605
634
|
const id = await pickDevClient({ client, slug: manifest.slug, ...(choose ? { choose } : {}), ...(ask ? { ask } : {}), log });
|
|
606
635
|
await rememberDevClient(projectDir, id);
|
|
@@ -624,7 +653,10 @@ export async function main(argv, { fetchImpl = fetch, log = console.log, runBuil
|
|
|
624
653
|
}
|
|
625
654
|
|
|
626
655
|
case 'data':
|
|
627
|
-
return dataCommand({
|
|
656
|
+
return dataCommand({
|
|
657
|
+
client, sub: positional[0], args: positional.slice(1), flags, ...(sleep ? { sleep } : {}), log,
|
|
658
|
+
...(env ? { env } : {}), manifest: () => loadManifest(manifestFile),
|
|
659
|
+
});
|
|
628
660
|
|
|
629
661
|
case 'secrets': {
|
|
630
662
|
const manifest = await loadManifest(manifestFile);
|
|
@@ -660,6 +692,14 @@ export async function main(argv, { fetchImpl = fetch, log = console.log, runBuil
|
|
|
660
692
|
return usageCommand({ client: client(), manifest: await loadManifest(manifestFile), flags, log });
|
|
661
693
|
}
|
|
662
694
|
|
|
695
|
+
case 'recordings': {
|
|
696
|
+
return recordingsCommand({ client: client(), manifest: await loadManifest(manifestFile), log });
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
case 'problems': {
|
|
700
|
+
return problemsCommand({ client: client(), manifest: await loadManifest(manifestFile), sub: positional[0], args: positional.slice(1), log });
|
|
701
|
+
}
|
|
702
|
+
|
|
663
703
|
case 'logs': {
|
|
664
704
|
const manifest = await loadManifest(manifestFile);
|
|
665
705
|
const controller = new AbortController();
|
|
@@ -852,7 +892,7 @@ export function parse(argv) {
|
|
|
852
892
|
flags.param = [...(flags.param ?? []), inline ?? argv[++i]];
|
|
853
893
|
continue;
|
|
854
894
|
}
|
|
855
|
-
const takesValue = ['manifest', 'platform', 'slug', 'name', 'template', 'client', 'app', 'since', 'value-from-env', 'engine', 'url', 'port', 'env', 'publisher', 'pick', 'channel', 'days', 'alias', 'explain'].includes(name);
|
|
895
|
+
const takesValue = ['manifest', 'platform', 'slug', 'name', 'template', 'client', 'app', 'since', 'value-from-env', 'engine', 'url', 'port', 'env', 'publisher', 'pick', 'channel', 'days', 'alias', 'explain', 'version', 'replay'].includes(name);
|
|
856
896
|
flags[name] = takesValue ? inline ?? argv[++i] : true;
|
|
857
897
|
} else if (command === undefined) {
|
|
858
898
|
command = arg;
|
package/src/data.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
3
|
import { checkValue, parseParamValue, validateQuery } from './queries.mjs';
|
|
4
|
+
import { formatShape, shapeOf } from './shape.mjs';
|
|
4
5
|
|
|
5
6
|
const MAX_ROWS_SHOWN = 50;
|
|
6
7
|
const MAX_CELL_WIDTH = 40;
|
|
@@ -11,7 +12,8 @@ const CLIENT_ID = /^[A-Za-z0-9]+$/;
|
|
|
11
12
|
const USAGE = `Usage:
|
|
12
13
|
hawk-app data tables [--app <slug>]
|
|
13
14
|
hawk-app data describe <table>
|
|
14
|
-
hawk-app data run <queries/name.sql> --client <id> [--param name=value …]
|
|
15
|
+
hawk-app data run <queries/name.sql> --client <id> [--param name=value …]
|
|
16
|
+
hawk-app data shape <queries/name.sql | report:<name>> [--client <id>] [--param name=value …]`;
|
|
15
17
|
|
|
16
18
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
19
|
|
|
@@ -19,7 +21,7 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
19
21
|
* `hawk-app data …`: the tables app queries can use, and running a query against a client's data before deploying it.
|
|
20
22
|
* `client` is a factory, so a usage mistake shows before any login check.
|
|
21
23
|
*/
|
|
22
|
-
export async function dataCommand({ client, sub, args = [], flags = {}, sleep = wait, log = console.log }) {
|
|
24
|
+
export async function dataCommand({ client, sub, args = [], flags = {}, sleep = wait, log = console.log, env = process.env, manifest = null, fetchImpl = fetch, today = () => new Date() }) {
|
|
23
25
|
switch (sub) {
|
|
24
26
|
case 'tables': {
|
|
25
27
|
const app = typeof flags.app === 'string' ? flags.app : undefined;
|
|
@@ -53,17 +55,69 @@ export async function dataCommand({ client, sub, args = [], flags = {}, sleep =
|
|
|
53
55
|
return 0;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
case 'run':
|
|
57
|
-
|
|
58
|
+
case 'run': {
|
|
59
|
+
const data = await execute({ client, file: args[0], clientId: flags.client, flags, sleep, log });
|
|
60
|
+
printTable(data.columns ?? [], data.rows ?? [], log);
|
|
61
|
+
const rowCount = data.rowCount ?? data.rows?.length ?? 0;
|
|
62
|
+
const shown = Math.min(rowCount, MAX_ROWS_SHOWN, data.rows?.length ?? 0);
|
|
63
|
+
log(`${rowCount} row${rowCount === 1 ? '' : 's'}${rowCount > shown ? ` (showing the first ${shown})` : ''}, ${formatBytes(data.bytesScanned ?? 0)} scanned${data.cached ? ' (cached result)' : ''}.`);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
case 'shape':
|
|
68
|
+
return shape({ client, source: args[0], flags, sleep, log, env, manifest, fetchImpl, today });
|
|
58
69
|
|
|
59
70
|
default:
|
|
60
71
|
throw new Error(sub ? `Unknown data command "${sub}".\n${USAGE}` : USAGE);
|
|
61
72
|
}
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
/**
|
|
76
|
+
* `hawk-app data shape <queries/name.sql | report:<name>>`: what a query or an analytics report answers for a client
|
|
77
|
+
* (default the app's dev client, HAWK_APPS_DEV_CLIENT), as its shape only: columns, types, how often empty, what values
|
|
78
|
+
* look like, bucketed sizes. Never the values: it's for the AI that writes the code (the MCP `data_shape` tool).
|
|
79
|
+
*/
|
|
80
|
+
async function shape({ client, source, flags, sleep, log, env, manifest, fetchImpl, today }) {
|
|
81
|
+
if (!source || !(source.endsWith('.sql') || source.startsWith('report:'))) throw new Error(USAGE);
|
|
82
|
+
const clientId = typeof flags.client === 'string' ? flags.client : env.HAWK_APPS_DEV_CLIENT;
|
|
83
|
+
if (!clientId || !CLIENT_ID.test(clientId)) {
|
|
84
|
+
throw new Error('Pass the client: --client <id> (or set HAWK_APPS_DEV_CLIENT in .env.local: `hawk-app dev` asks once and saves it).');
|
|
85
|
+
}
|
|
86
|
+
let result;
|
|
87
|
+
if (source.endsWith('.sql')) {
|
|
88
|
+
const data = await execute({ client, file: source, clientId, flags, sleep, log: () => undefined });
|
|
89
|
+
result = shapeOf(data.rows ?? [], data.columns ?? []);
|
|
90
|
+
} else {
|
|
91
|
+
const app = manifest ? await manifest() : null;
|
|
92
|
+
if (!app?.slug) throw new Error('Run it in the app\'s folder (next to hawk-app.json): reports are called as the app.');
|
|
93
|
+
const name = source.slice('report:'.length).replace(/^\/+|\/+$/g, '');
|
|
94
|
+
if (!/^[A-Za-z0-9/_-]+$/.test(name)) throw new Error(`"${name}" isn't a report name (analytics_reports lists them).`);
|
|
95
|
+
const end = today();
|
|
96
|
+
const from = new Date(end.getTime() - 29 * 86_400_000).toISOString().slice(0, 10);
|
|
97
|
+
const body = { from, to: end.toISOString().slice(0, 10) };
|
|
98
|
+
for (const entry of flags.param ?? []) {
|
|
99
|
+
const eq = typeof entry === 'string' ? entry.indexOf('=') : -1;
|
|
100
|
+
if (eq <= 0) throw new Error(`--param needs name=value, not "${entry}".`);
|
|
101
|
+
const raw = entry.slice(eq + 1);
|
|
102
|
+
body[entry.slice(0, eq)] = /^-?\d+(\.\d+)?$/.test(raw) ? Number(raw) : /^(true|false)$/.test(raw) ? raw === 'true' : raw;
|
|
103
|
+
}
|
|
104
|
+
const issued = await client().devToken(app.slug, { clientId, engineId: env.HAWK_APPS_DEV_ENGINE || null, internal: true });
|
|
105
|
+
const url = new URL(`analytics/${name}`, issued.apiBaseUrl.endsWith('/') ? issued.apiBaseUrl : `${issued.apiBaseUrl}/`);
|
|
106
|
+
const response = await fetchImpl(url, { method: 'POST', headers: { Authorization: `Bearer ${issued.token}`, 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify(body) });
|
|
107
|
+
const payload = await response.json().catch(() => null);
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(`The report answered ${response.status}${payload?.code ? ` ${payload.code}` : ''}${payload?.detail ? `: ${payload.detail}` : ''}.`);
|
|
110
|
+
}
|
|
111
|
+
const rows = Array.isArray(payload?.data) ? payload.data : Array.isArray(payload) ? payload : [];
|
|
112
|
+
result = shapeOf(rows);
|
|
113
|
+
}
|
|
114
|
+
for (const line of formatShape(`${source} for client ${clientId}`, result)) log(line);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Checks and runs a query file against a client; resolves with the platform's answer (columns, rows, …). */
|
|
119
|
+
async function execute({ client, file, clientId, flags, sleep, log }) {
|
|
65
120
|
if (!file || !file.endsWith('.sql')) throw new Error(USAGE);
|
|
66
|
-
const clientId = flags.client;
|
|
67
121
|
if (typeof clientId !== 'string' || !CLIENT_ID.test(clientId)) throw new Error('Pass the client to run against: --client <id> (letters and digits, e.g. e3141).');
|
|
68
122
|
|
|
69
123
|
const jsonFile = file.replace(/\.sql$/, '.json');
|
|
@@ -128,13 +182,7 @@ async function run({ client, file, flags, sleep, log }) {
|
|
|
128
182
|
await sleep(Math.min(Math.max(1000 * 2 ** attempt, data.retryAfterMs ?? 0), MAX_POLL_DELAY_MS));
|
|
129
183
|
({ status, data } = await retrying(() => platform.dataExecution(data.executionId)));
|
|
130
184
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
printTable(data.columns ?? [], data.rows ?? [], log);
|
|
134
|
-
const rowCount = data.rowCount ?? data.rows?.length ?? 0;
|
|
135
|
-
const shown = Math.min(rowCount, MAX_ROWS_SHOWN, data.rows?.length ?? 0);
|
|
136
|
-
log(`${rowCount} row${rowCount === 1 ? '' : 's'}${rowCount > shown ? ` (showing the first ${shown})` : ''}, ${formatBytes(data.bytesScanned ?? 0)} scanned${data.cached ? ' (cached result)' : ''}.`);
|
|
137
|
-
return 0;
|
|
185
|
+
return data;
|
|
138
186
|
}
|
|
139
187
|
|
|
140
188
|
async function readText(file) {
|