@myapihq/cli 2.14.0 → 2.15.0

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.
@@ -6,7 +6,7 @@
6
6
  // because nothing they read mentioned it.
7
7
  import { feedback as sdkFeedback } from '@myapihq/sdk';
8
8
  import { requireConfig } from '../config.js';
9
- import { success, error, info, printTable, printJson, banner } from '../output.js';
9
+ import { success, error, info, printJson, banner } from '../output.js';
10
10
  import { formatDate } from '../utils.js';
11
11
  import { requireOrg, requireArg, confirmDestructive } from '../helpers.js';
12
12
  export const EXPOSES = [
@@ -20,6 +20,7 @@ export const EXPOSES = [
20
20
  'DELETE /feedback/orgs/{org_id}/widgets/{id}',
21
21
  ];
22
22
  export const SCHEMA = {
23
+ trace: 'boolean',
23
24
  routes: 'string',
24
25
  accent: 'string',
25
26
  position: 'string',
@@ -33,6 +34,28 @@ export const SCHEMA = {
33
34
  };
34
35
  // The platform's enum, checked against the schema rather than invented.
35
36
  const KINDS = ['bug', 'issue', 'suggestion'];
37
+ // A selector says `div#install > button.save`. The context says the button read
38
+ // "Save settings" and sat under "Install". The second is what a person needs,
39
+ // so prefer it and fall back to the selector only when it is absent.
40
+ function describeTarget(i) {
41
+ const ctx = i.target_context;
42
+ const one = ctx && !ctx.region ? ctx : (ctx?.region?.[0]);
43
+ if (one) {
44
+ const label = one.text || one.aria_label || one.name || one.id;
45
+ const bits = [];
46
+ if (label)
47
+ bits.push(JSON.stringify(label));
48
+ if (one.tag)
49
+ bits.push(`(${one.tag})`);
50
+ let s = bits.join(' ') || one.tag || '';
51
+ if (one.section)
52
+ s += ` under ${JSON.stringify(one.section)}`;
53
+ const more = ctx?.region?.length > 1 ? ` +${ctx.region.length - 1} more` : '';
54
+ if (s)
55
+ return s + more;
56
+ }
57
+ return i.target_selector ?? '';
58
+ }
36
59
  export async function list(flags) {
37
60
  const config = requireConfig();
38
61
  const orgId = requireOrg(flags, config, 'myapi feedback list [--kind <k>] [--status open|resolved] [--org <id>]');
@@ -56,23 +79,54 @@ export async function list(flags) {
56
79
  // that explicitly, having been bitten by the opposite in CRM search.
57
80
  const shown = page.items?.length ?? 0;
58
81
  info(shown === page.total ? `${shown} item${shown === 1 ? '' : 's'}` : `${shown} of ${page.total} items`);
59
- // The table cannot carry a signed URL, a selector and a navigation trace, so
60
- // it flags what exists and --json carries it. Saying nothing about a
61
- // screenshot would leave a picture nobody knows to look for.
62
- printTable((page.items ?? []).map(i => ({
63
- id: i.id,
64
- kind: i.kind,
65
- status: i.status,
66
- body: i.body.length > 60 ? `${i.body.slice(0, 57)}…` : i.body,
67
- route: i.route ?? i.page_url ?? '',
68
- // A failed signature still means a picture is there — distinguish that
69
- // from having none, rather than showing both as blank.
70
- shot: i.screenshot_url ? 'yes' : (i.screenshot_asset_id ? 'unsigned' : ''),
71
- created: i.created_at ? formatDate(i.created_at) : '',
72
- })), { flags, empty: 'No feedback yet. Put a widget on a page: myapi feedback widget create <name>' });
82
+ info('');
83
+ // A report used to be one line of prose with a CSS selector under it. It now
84
+ // carries what was pointed at, what happened before, and a picture and a
85
+ // table cannot hold any of that. Twenty of these dumped in full would be
86
+ // unreadable, so each item summarises and `--trace` expands.
87
+ for (const i of page.items ?? []) {
88
+ const when = i.created_at ? formatDate(i.created_at) : '';
89
+ info(`${i.kind.padEnd(10)} ${when} ${i.route ?? i.page_url ?? ''}`);
90
+ info(` ${JSON.stringify(i.body.length > 100 ? `${i.body.slice(0, 97)}…` : i.body)}`);
91
+ const pointing = describeTarget(i);
92
+ if (pointing)
93
+ info(` pointing at ${pointing}`);
94
+ const trace = Array.isArray(i.trace) ? i.trace : [];
95
+ if (trace.length) {
96
+ // Errors and failed requests are the highest-signal part — for most
97
+ // reports the answer is a 500 nobody saw. Counting them beats truncating
98
+ // the trace into the summary line.
99
+ const errors = trace.filter(e => e.kind === 'error').length;
100
+ const failed = trace.filter(e => e.kind === 'request' && /\b[45]\d\d\b/.test(e.detail)).length;
101
+ const parts = [`${trace.length} step${trace.length === 1 ? '' : 's'}`];
102
+ if (errors)
103
+ parts.push(`${errors} error${errors === 1 ? '' : 's'}`);
104
+ if (failed)
105
+ parts.push(`${failed} failed request${failed === 1 ? '' : 's'}`);
106
+ info(` before ${parts.join(' · ')}`);
107
+ }
108
+ if (i.screenshot_url)
109
+ info(' screenshot yes');
110
+ else if (i.screenshot_asset_id)
111
+ info(' screenshot yes (link could not be signed)');
112
+ if (flags.trace && trace.length) {
113
+ info('');
114
+ for (const e of trace) {
115
+ // `t` is ms since page load, not wall clock — rendered as a delta so
116
+ // nobody reads it as a time of day.
117
+ info(` +${(e.t / 1000).toFixed(1)}s ${String(e.kind).padEnd(8)} ${e.detail}`);
118
+ }
119
+ }
120
+ info(` id ${i.id}`);
121
+ info('');
122
+ }
123
+ if (!shown)
124
+ info('No feedback yet. Put a widget on a page: myapi feedback widget create <name>');
73
125
  const withShots = (page.items ?? []).filter(i => i.screenshot_url || i.screenshot_asset_id).length;
74
- if (withShots > 0 && !flags.json) {
75
- info(`${withShots} item${withShots === 1 ? ' has' : 's have'} a screenshot — \`--json\` carries the link (signed, ~1h).`);
126
+ if (withShots > 0)
127
+ info(`\`--json\` carries the screenshot links (signed, ~1h — do not cache them).`);
128
+ if ((page.items ?? []).some(i => Array.isArray(i.trace) && i.trace.length) && !flags.trace) {
129
+ info('`--trace` expands what happened before each report.');
76
130
  }
77
131
  if (page.has_more)
78
132
  info('More available — raise --limit or pass --offset.');
@@ -116,9 +170,12 @@ export async function del(id, flags) {
116
170
  requireArg(id, 'id', usage);
117
171
  // Erases rather than closes. `resolve` is the reversible one; this is not,
118
172
  // so it takes the same confirmation every destructive verb here takes.
119
- await confirmDestructive(flags, `permanently delete feedback ${id} (resolve keeps it; this erases it)`, usage);
173
+ await confirmDestructive(flags, `permanently delete feedback ${id} — the report AND its screenshot (resolve keeps both)`, usage);
120
174
  await sdkFeedback.deleteFeedback(config.api_key, orgId, id);
121
175
  success(`Deleted ${id}`);
176
+ // The cascade is the point: somebody deleting a report because of what is IN
177
+ // it needs the picture gone too, and it used to be left behind.
178
+ info('The stored screenshot went with it.');
122
179
  info('(Idempotent — an unknown id answers the same way, so this is not confirmation the item existed.)');
123
180
  }
124
181
  export async function widget(sub, arg, flags) {
package/dist/errors.js CHANGED
@@ -26,6 +26,7 @@ export const ERROR_MESSAGES = {
26
26
  invalid_org: 'Invalid organization.',
27
27
  FORBIDDEN: 'You do not have permission to perform this action.',
28
28
  RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
29
+ BODY_TOO_LARGE: 'The request is over 64 KB — usually an oversized feedback `trace`. Nothing was saved. This used to surface as INVALID_JSON, which sent people looking for a syntax error they did not have.',
29
30
  INSUFFICIENT_BALANCE: 'Insufficient balance. Run: myapi billing topup <amount>',
30
31
  INVALID_AMOUNT: 'Amount out of range. Maximum single top-up is $100. Run: myapi billing topup <amount>',
31
32
  SERVICE_NOT_LAUNCHED: 'This service is disabled pre-launch. Track availability via: myapi status',
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: my-feedback-api
3
- version: 1.4.0
3
+ version: 1.5.0
4
4
  description: >
5
5
  Collect feedback from the people using what you built. A public widget key lets a page submit without a credential; you list, filter and resolve the results. Kind is chosen by the person reporting, not inferred from their wording.
6
6
  triggers: [feedback, bug report, user feedback, feature request, widget, support, complaints, praise]
7
- checksum: sha256-b8a6dbed6fa4aa393e538225ae611f5f5d8c4fbb1ffea7922e4a0286d984eca4
7
+ checksum: sha256-fdd06435d3f56b8ff220144771731b60694ed107835b3869d96571f46e3f3653
8
8
  ---
9
9
 
10
10
  # MyFeedbackAPI
@@ -44,9 +44,11 @@ theme baked in. This is the intended way in — you do not build a UI:
44
44
 
45
45
  It renders a button, shows itself only on the routes the widget is configured
46
46
  for (set them with `widget update <id> --routes /app,/app/**`; changes reach
47
- browsers within ~5 minutes with no redeploy of your site), and lets the person point at an element or drag a region. It sends `kind`,
48
- `body`, `page_url`, `route`, `viewport`, plus **either** `target_selector` (they
49
- clicked something) **or** `target_region` `{x,y,w,h}` (they dragged a box).
47
+ browsers within ~5 minutes with no redeploy of your site), and lets the person point at an element or drag a region. It sends `kind`, `body`, `page_url`, `route`, `viewport`, `target_selector`,
48
+ and `target_region` `{x,y,w,h}` the region is on **every** report now, the
49
+ element's bounding rectangle for a click and the dragged box for a drag. It also
50
+ sends `target_context` (what the element *was* — its text, role, the heading
51
+ above it) and `trace`, the last events before the report.
50
52
 
51
53
  A wrong key does not break the page: the response is `application/javascript`
52
54
  carrying a JS comment that names the fix, so a typo cannot throw a syntax error
@@ -92,10 +94,19 @@ processed signal.
92
94
 
93
95
  ### Reading it back
94
96
 
95
- Each item also carries what was pointed at (`target_selector`, `target_region`,
96
- `target_context`), how they got there (`trace`), the `viewport`, and
97
- `screenshot_url` when there is a picture. The table flags which items have one;
98
- `--json` carries the link.
97
+ Each item carries what was pointed at, how they got there, and a picture.
98
+ `myapi feedback list` summarises that per report the element's own text rather
99
+ than its CSS selector, a count of steps/errors/failed requests, and whether a
100
+ screenshot exists. `--trace` expands the event list; `--json` passes everything
101
+ through.
102
+
103
+ Two things not to misread: **`trace[].t` is milliseconds since page load**, not
104
+ a clock time, so it only means anything as a delta within one trace. And
105
+ **`target_context.filled` says an input had a value, never what it was** — the
106
+ widget does not collect it.
107
+
108
+ `delete` removes the screenshot along with the report, which is the case that
109
+ matters: somebody erasing a report because of what is in the picture.
99
110
 
100
111
  `myapi feedback list` is newest first, filterable by `--kind` and `--status`
101
112
  (`open` | `resolved`), paged with `--limit` / `--offset`. **`total` is the
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
3
  "license": "Apache-2.0",
4
- "version": "2.14.0",
4
+ "version": "2.15.0",
5
5
  "description": "MyAPI command-line interface",
6
6
  "repository": {
7
7
  "type": "git",
@@ -46,7 +46,7 @@
46
46
  "lint:skills:strict": "node scripts/copy-skills.js && node scripts/lint-skills.js --strict"
47
47
  },
48
48
  "dependencies": {
49
- "@myapihq/sdk": "^2.14.0"
49
+ "@myapihq/sdk": "^2.15.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^25.6.0",