@myapihq/cli 2.13.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,14 +79,55 @@ 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
- printTable((page.items ?? []).map(i => ({
60
- id: i.id,
61
- kind: i.kind,
62
- status: i.status,
63
- body: i.body.length > 60 ? `${i.body.slice(0, 57)}…` : i.body,
64
- route: i.route ?? i.page_url ?? '',
65
- created: i.created_at ? formatDate(i.created_at) : '',
66
- })), { 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>');
125
+ const withShots = (page.items ?? []).filter(i => i.screenshot_url || i.screenshot_asset_id).length;
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.');
130
+ }
67
131
  if (page.has_more)
68
132
  info('More available — raise --limit or pass --offset.');
69
133
  }
@@ -106,9 +170,12 @@ export async function del(id, flags) {
106
170
  requireArg(id, 'id', usage);
107
171
  // Erases rather than closes. `resolve` is the reversible one; this is not,
108
172
  // so it takes the same confirmation every destructive verb here takes.
109
- 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);
110
174
  await sdkFeedback.deleteFeedback(config.api_key, orgId, id);
111
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.');
112
179
  info('(Idempotent — an unknown id answers the same way, so this is not confirmation the item existed.)');
113
180
  }
114
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.3.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-de137a7db55ba3173aecdd489b4568e66166a0d6c145f5a9fa165c5ef252bc01
7
+ checksum: sha256-fdd06435d3f56b8ff220144771731b60694ed107835b3869d96571f46e3f3653
8
8
  ---
9
9
 
10
10
  # MyFeedbackAPI
@@ -44,17 +44,25 @@ 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
53
55
  in your app.
54
56
 
55
- **No screenshots.** The submit body takes no image and there is no multipart
56
- form `target_selector` / `target_region` are the platform's answer to "where
57
- were they pointing". Do not build a capture step expecting somewhere to put it.
57
+ **Screenshots are automatic you do not build a capture step.** When someone
58
+ opens the feedback sheet the widget fetches a renderer on demand and attaches
59
+ the picture itself, as a second call after the report is saved. That ordering is
60
+ deliberate: an image that fails to render or upload never costs the report.
61
+
62
+ The image is **stored private in your org's assets**, and listing feedback
63
+ returns a **signed link good for about an hour** (`screenshot_url`). It is
64
+ signed on read, so it is not stable — re-list rather than persisting it. PNG,
65
+ JPEG or WebP up to 3 MB; over that the feedback is kept without the picture.
58
66
 
59
67
  ### Submitting by hand (only if you cannot use the script)
60
68
 
@@ -86,6 +94,20 @@ processed signal.
86
94
 
87
95
  ### Reading it back
88
96
 
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.
110
+
89
111
  `myapi feedback list` is newest first, filterable by `--kind` and `--status`
90
112
  (`open` | `resolved`), paged with `--limit` / `--offset`. **`total` is the
91
113
  number of matches, not the size of the page**, and `has_more` flags a truncated
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
3
  "license": "Apache-2.0",
4
- "version": "2.13.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.13.0"
49
+ "@myapihq/sdk": "^2.15.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^25.6.0",