@shipeasy/openapi 3.2.0 → 3.4.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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-RTN572TI.js → chunk-JJA7XEPU.js} +91 -1
  3. package/dist/chunk-JJA7XEPU.js.map +1 -0
  4. package/dist/{chunk-DW33MG4B.js → chunk-Z3CY5Y44.js} +514 -126
  5. package/dist/chunk-Z3CY5Y44.js.map +1 -0
  6. package/dist/{client-BPVn4hMC.d.ts → client-CkRbr8I2.d.ts} +1544 -212
  7. package/dist/client.d.ts +1 -1
  8. package/dist/client.js +23 -1
  9. package/dist/custom.d.ts +5 -5
  10. package/dist/custom.js +128 -84
  11. package/dist/custom.js.map +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.js +24 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/schemas.d.ts +1935 -963
  16. package/dist/schemas.js +141 -1
  17. package/openapi-sdk.json +31 -9
  18. package/openapi.json +10449 -8118
  19. package/openapi.yaml +2274 -634
  20. package/package.json +1 -1
  21. package/spec/components/schemas/alert-rules.yaml +434 -10
  22. package/spec/components/schemas/alerts.yaml +9 -7
  23. package/spec/components/schemas/common.yaml +2 -2
  24. package/spec/components/schemas/connectors.yaml +69 -6
  25. package/spec/components/schemas/events.yaml +20 -9
  26. package/spec/components/schemas/keys.yaml +39 -0
  27. package/spec/components/schemas/metrics.yaml +131 -251
  28. package/spec/components/schemas/ops.yaml +471 -29
  29. package/spec/components/schemas/projects.yaml +18 -0
  30. package/spec/components/schemas/search.yaml +1 -0
  31. package/spec/components/schemas/tags.yaml +91 -0
  32. package/spec/openapi.yaml +56 -0
  33. package/spec/paths/alerts.yaml +5 -3
  34. package/spec/paths/i18n.yaml +2 -1
  35. package/spec/paths/keys.yaml +79 -0
  36. package/spec/paths/metrics.yaml +26 -17
  37. package/spec/paths/ops.yaml +514 -11
  38. package/spec/paths/tags.yaml +270 -0
  39. package/dist/chunk-DW33MG4B.js.map +0 -1
  40. package/dist/chunk-RTN572TI.js.map +0 -1
package/dist/client.d.ts CHANGED
@@ -1 +1 @@
1
- export { w as Client, y as Config, B as ConfigureOptions, Z as CreateClientConfig, jl as Options, jY as RequestResult, o0 as ackOpsItem, o1 as addToGateWhitelist, o2 as approveEvent, o3 as client, o4 as configure, o5 as createAlertRule, o6 as createAttribute, o7 as createClient, o8 as createClientConfig, o9 as createConfig, oa as createConnector, ob as createEvent, oc as createExperiment, od as createExperimentReadout, oe as createGate, of as createGateTemplate, og as createI18nDraft, oh as createI18nProfile, oi as createKey, oj as createKillswitch, ok as createMetric, ol as createOpsComment, om as createOpsInvestigation, on as createOpsItem, oo as createPublicBug, op as createPublicFeatureRequest, oq as createTriggerConnector, or as createUniverse, os as deleteAlertRule, ot as deleteAttribute, ou as deleteConfig, ov as deleteConnector, ow as deleteEvent, ox as deleteExperiment, oy as deleteGate, oz as deleteGateTemplate, oA as deleteI18nDraft, oB as deleteI18nKey, oC as deleteI18nProfile, oD as deleteKillswitch, oE as deleteMetric, oF as deleteOpsItem, oG as deleteUniverse, oH as disableGate, oI as discardConfigDraft, oJ as enableGate, oK as fileErrorTicket, oL as fireConnector, oM as getAttribute, oN as getConfig, oO as getConnector, oP as getCurrentProject, oQ as getError, oR as getErrorSeries, oS as getEvent, oT as getExperiment, oU as getExperimentReadout, oV as getExperimentResults, oW as getExperimentTimeseries, oX as getGate, oY as getGateTemplate, oZ as getGateWhitelist, o_ as getKillswitch, o$ as getMetric, p0 as getMetricSeries, p1 as getOpsItem, p2 as getProject, p3 as linkPrToOpsItem, p4 as listAlertRules, p5 as listAlerts, p6 as listAttributes, p7 as listConfigActivity, p8 as listConfigVersions, p9 as listConfigs, pa as listConnectors, pb as listErrors, pc as listEvents, pd as listExperiments, pe as listGateActivity, pf as listGateTemplates, pg as listGates, ph as listI18nDraftKeys, pi as listI18nDrafts, pj as listI18nKeys, pk as listI18nProfiles, pl as listKeys, pm as listKillswitches, pn as listMetricExperiments, po as listMetrics, pp as listOpsAgents, pq as listOpsComments, pr as listOpsInvestigations, ps as listOpsItems, pt as listSlackChannels, pu as listUniverses, pv as notifyOps, pw as publishConfigDraft, px as publishI18nProfile, py as pushI18nKeys, pz as reanalyzeExperiment, pA as removeFromGateWhitelist, pB as resolveError, pC as revokeKey, pD as saveConfigDraft, pE as searchResources, pF as setExperimentMetrics, pG as setExperimentStatus, pH as setGateWhitelist, pI as setI18nLabel, pJ as setKillswitchSwitch, pK as setKillswitchValue, pL as testConnector, pM as toggleKillswitch, pN as unarchiveMetric, pO as unsetKillswitchSwitch, pP as updateAlert, pQ as updateAlertRule, pR as updateAttribute, pS as updateConfig, pT as updateConfigSchema, pU as updateConnector, pV as updateErrorStatus, pW as updateEvent, pX as updateExperiment, pY as updateGate, pZ as updateGateTemplate, p_ as updateI18nDraft, p$ as updateI18nKey, q0 as updateKillswitch, q1 as updateMetric, q2 as updateOpsInvestigation, q3 as updateOpsItem, q4 as updateProject, q5 as updateTriggerConnector, q6 as updateUniverse, q7 as upsertI18nDraftKey, q8 as upsertI18nKeys, q9 as upsertProject } from './client-BPVn4hMC.js';
1
+ export { U as Client, W as Config, Y as ConfigureOptions, aj as CreateClientConfig, kk as Options, kX as RequestResult, pk as ackOpsItem, pl as addToGateWhitelist, pm as answerOpsQuestion, pn as approveEvent, po as askOpsQuestions, pp as cancelOpsQuestion, pq as client, pr as configure, ps as createAlertRule, pt as createAttribute, pu as createClient, pv as createClientConfig, pw as createConfig, px as createConnector, py as createEvent, pz as createExperiment, pA as createExperimentReadout, pB as createGate, pC as createGateTemplate, pD as createI18nDraft, pE as createI18nProfile, pF as createKey, pG as createKillswitch, pH as createMetric, pI as createOpsComment, pJ as createOpsInvestigation, pK as createOpsItem, pL as createOpsQaNote, pM as createPublicBug, pN as createPublicFeatureRequest, pO as createTriggerConnector, pP as createUniverse, pQ as deleteAlertRule, pR as deleteAttribute, pS as deleteConfig, pT as deleteConnector, pU as deleteEvent, pV as deleteExperiment, pW as deleteGate, pX as deleteGateTemplate, pY as deleteI18nDraft, pZ as deleteI18nKey, p_ as deleteI18nProfile, p$ as deleteKillswitch, q0 as deleteMetric, q1 as deleteOpsItem, q2 as deleteUniverse, q3 as disableGate, q4 as discardConfigDraft, q5 as enableGate, q6 as fileErrorTicket, q7 as fireConnector, q8 as getAttribute, q9 as getConfig, qa as getConnector, qb as getCurrentProject, qc as getEntityTags, qd as getError, qe as getErrorSeries, qf as getEvent, qg as getExperiment, qh as getExperimentReadout, qi as getExperimentResults, qj as getExperimentTimeseries, qk as getGate, ql as getGateTemplate, qm as getGateWhitelist, qn as getKillswitch, qo as getMetric, qp as getMetricSeries, qq as getOpsItem, qr as getProject, qs as linkPrToOpsItem, qt as listAlertRules, qu as listAlerts, qv as listAttributes, qw as listConfigActivity, qx as listConfigVersions, qy as listConfigs, qz as listConnectors, qA as listErrors, qB as listEvents, qC as listExperiments, qD as listGateActivity, qE as listGateTemplates, qF as listGates, qG as listI18nDraftKeys, qH as listI18nDrafts, qI as listI18nKeys, qJ as listI18nProfiles, qK as listKeys, qL as listKillswitches, qM as listMetricExperiments, qN as listMetrics, qO as listOpsAgents, qP as listOpsComments, qQ as listOpsInvestigations, qR as listOpsItems, qS as listOpsQaNotes, qT as listOpsQuestions, qU as listSlackChannels, qV as listTags, qW as listUniverses, qX as notifyOps, qY as publishConfigDraft, qZ as publishI18nProfile, q_ as pushI18nKeys, q$ as reanalyzeExperiment, r0 as removeFromGateWhitelist, r1 as resolveError, r2 as revokeKey, r3 as saveConfigDraft, r4 as searchResources, r5 as setEntityTags, r6 as setExperimentMetrics, r7 as setExperimentStatus, r8 as setGateWhitelist, r9 as setI18nLabel, ra as setKillswitchSwitch, rb as setKillswitchValue, rc as testConnector, rd as toggleKillswitch, re as unarchiveMetric, rf as unsetKillswitchSwitch, rg as updateAlert, rh as updateAlertRule, ri as updateAttribute, rj as updateConfig, rk as updateConfigSchema, rl as updateConnector, rm as updateEntityTags, rn as updateErrorStatus, ro as updateEvent, rp as updateExperiment, rq as updateGate, rr as updateGateTemplate, rs as updateI18nDraft, rt as updateI18nKey, ru as updateKey, rv as updateKillswitch, rw as updateMetric, rx as updateOpsInvestigation, ry as updateOpsItem, rz as updateProject, rA as updateTriggerConnector, rB as updateUniverse, rC as upsertI18nDraftKey, rD as upsertI18nKeys, rE as upsertProject } from './client-CkRbr8I2.js';
package/dist/client.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import {
2
2
  ackOpsItem,
3
3
  addToGateWhitelist,
4
+ answerOpsQuestion,
4
5
  approveEvent,
6
+ askOpsQuestions,
7
+ cancelOpsQuestion,
5
8
  client,
6
9
  configure,
7
10
  createAlertRule,
@@ -23,6 +26,7 @@ import {
23
26
  createOpsComment,
24
27
  createOpsInvestigation,
25
28
  createOpsItem,
29
+ createOpsQaNote,
26
30
  createPublicBug,
27
31
  createPublicFeatureRequest,
28
32
  createTriggerConnector,
@@ -51,6 +55,7 @@ import {
51
55
  getConfig,
52
56
  getConnector,
53
57
  getCurrentProject,
58
+ getEntityTags,
54
59
  getError,
55
60
  getErrorSeries,
56
61
  getEvent,
@@ -92,7 +97,10 @@ import {
92
97
  listOpsComments,
93
98
  listOpsInvestigations,
94
99
  listOpsItems,
100
+ listOpsQaNotes,
101
+ listOpsQuestions,
95
102
  listSlackChannels,
103
+ listTags,
96
104
  listUniverses,
97
105
  notifyOps,
98
106
  publishConfigDraft,
@@ -104,6 +112,7 @@ import {
104
112
  revokeKey,
105
113
  saveConfigDraft,
106
114
  searchResources,
115
+ setEntityTags,
107
116
  setExperimentMetrics,
108
117
  setExperimentStatus,
109
118
  setGateWhitelist,
@@ -120,6 +129,7 @@ import {
120
129
  updateConfig,
121
130
  updateConfigSchema,
122
131
  updateConnector,
132
+ updateEntityTags,
123
133
  updateErrorStatus,
124
134
  updateEvent,
125
135
  updateExperiment,
@@ -127,6 +137,7 @@ import {
127
137
  updateGateTemplate,
128
138
  updateI18nDraft,
129
139
  updateI18nKey,
140
+ updateKey,
130
141
  updateKillswitch,
131
142
  updateMetric,
132
143
  updateOpsInvestigation,
@@ -137,11 +148,14 @@ import {
137
148
  upsertI18nDraftKey,
138
149
  upsertI18nKeys,
139
150
  upsertProject
140
- } from "./chunk-RTN572TI.js";
151
+ } from "./chunk-JJA7XEPU.js";
141
152
  export {
142
153
  ackOpsItem,
143
154
  addToGateWhitelist,
155
+ answerOpsQuestion,
144
156
  approveEvent,
157
+ askOpsQuestions,
158
+ cancelOpsQuestion,
145
159
  client,
146
160
  configure,
147
161
  createAlertRule,
@@ -163,6 +177,7 @@ export {
163
177
  createOpsComment,
164
178
  createOpsInvestigation,
165
179
  createOpsItem,
180
+ createOpsQaNote,
166
181
  createPublicBug,
167
182
  createPublicFeatureRequest,
168
183
  createTriggerConnector,
@@ -191,6 +206,7 @@ export {
191
206
  getConfig,
192
207
  getConnector,
193
208
  getCurrentProject,
209
+ getEntityTags,
194
210
  getError,
195
211
  getErrorSeries,
196
212
  getEvent,
@@ -232,7 +248,10 @@ export {
232
248
  listOpsComments,
233
249
  listOpsInvestigations,
234
250
  listOpsItems,
251
+ listOpsQaNotes,
252
+ listOpsQuestions,
235
253
  listSlackChannels,
254
+ listTags,
236
255
  listUniverses,
237
256
  notifyOps,
238
257
  publishConfigDraft,
@@ -244,6 +263,7 @@ export {
244
263
  revokeKey,
245
264
  saveConfigDraft,
246
265
  searchResources,
266
+ setEntityTags,
247
267
  setExperimentMetrics,
248
268
  setExperimentStatus,
249
269
  setGateWhitelist,
@@ -260,6 +280,7 @@ export {
260
280
  updateConfig,
261
281
  updateConfigSchema,
262
282
  updateConnector,
283
+ updateEntityTags,
263
284
  updateErrorStatus,
264
285
  updateEvent,
265
286
  updateExperiment,
@@ -267,6 +288,7 @@ export {
267
288
  updateGateTemplate,
268
289
  updateI18nDraft,
269
290
  updateI18nKey,
291
+ updateKey,
270
292
  updateKillswitch,
271
293
  updateMetric,
272
294
  updateOpsInvestigation,
package/dist/custom.d.ts CHANGED
@@ -68,12 +68,12 @@ declare const opId: (op: CustomOp) => string;
68
68
  * examples.
69
69
  *
70
70
  * Source of truth for semantics is the RUNTIME parser + IR that the server
71
- * actually runs: the main repo's `packages/query-dsl` (`parse.ts` / `ir.ts`).
72
- * The vendored `cli/src/query-dsl` copy is a lagging subset (no `ratio` yet) and
73
- * is NOT on the create path — the CLI forwards the raw `--query` string to the
74
- * server, which parses it. Keep this text in sync with `packages/query-dsl`.
71
+ * actually runs: the main repo's `packages/query-dsl` (`parse-expr.ts` /
72
+ * `expr.ts`). The CLI forwards the raw `--query` string to the server, which
73
+ * parses it — there is no second parser here to keep in step, only this text,
74
+ * which has to stay in sync with `packages/query-dsl`.
75
75
  */
76
- declare const METRIC_GRAMMAR = "SHIPEASY METRIC QUERY DSL\n=========================\n\nA metric is an aggregation over an event stream. Almost always that is ONE\naggregation over ONE event; the single exception is the binary `ratio(...)`\nform, which divides one count-style arm by another (each arm its own event and\nfilters) in one pass. This DSL is the string you pass to\n`metrics create --query \"...\"` (equivalently the typed `query_ir`). There is\nno general arithmetic, no free-form formulas, and no subqueries \u2014 `ratio` is\nthe only division / two-event form.\n\n\nGRAMMAR (BNF)\n-------------\n Query := Agg | Ratio\n Agg := AggFunc \"(\" Selector (\",\" Identifier)? \")\" GroupBy?\n Ratio := \"ratio\" \"(\" RatioArm \",\" RatioArm \")\"\n RatioArm := (\"count_users\" | \"count\") \"(\" Selector \")\"\n AggFunc := \"count_users\" | \"count\" | \"sum\" | \"avg\" | \"min\" | \"max\"\n | \"unique\" | \"p50\" | \"p75\" | \"p90\" | \"p95\" | \"p99\" | \"p999\"\n | \"retention_\" <N> \"d\" (N = 1..90)\n Selector := Identifier (\"{\" Filter (\",\" Filter)* \",\"? \"}\")?\n Filter := Identifier MatchOp StringLiteral\n MatchOp := \"=\" | \"!=\" | \"=~\" | \"!~\"\n GroupBy := (\"by\" | \"without\") \"(\" Identifier (\",\" Identifier)* \",\"? \")\"\n StringLiteral := double-quoted; \\\" is a literal quote, \\\\ is a backslash\n Identifier := [A-Za-z_][A-Za-z0-9_]* (case-sensitive)\n\n The trailing Identifier after the selector \u2014 \"(\" Selector \",\" Identifier \")\" \u2014\n is the VALUE LABEL: the numeric event label the aggregation reduces. Whether it\n is required, optional, or forbidden depends on the aggregation (see below).\n\n\nAGGREGATIONS\n------------\n Function Value label Meaning In experiments\n -------------- ------------ ------------------------------ ---------------\n count_users forbidden distinct users who fired it exact\n count forbidden number of events (rows) exact\n sum(e, v) required \u03A3 of numeric label v exact\n avg(e, v) required mean of label v exact\n retention_Nd(e) forbidden % of users who returned exact\n within N days (N = 1..90)\n min(e, v) required minimum of label v approx \u2192 avg *\n max(e, v) required maximum of label v approx \u2192 avg *\n unique(e, v) required distinct values of label v approx \u2192 avg *\n p50 \u2026 p999(e,v) required quantile of v (50/75/90/ approx \u2192 avg *\n 95/99/99.9th percentile)\n ratio(a, b) forbidden a \u00F7 b (see RATIO below) proportion **\n\n * Display-only aggregations. The experiment t-test needs a per-user mean and\n variance, so min/max/unique/quantile are computed exactly on dashboards but\n collapse to a per-user `avg` when used as an experiment metric. If you need\n an exact experiment metric, pick count_users / count / sum / avg / retention.\n\n ** In an experiment a ratio collapses to a per-user 0/1 outcome (a proportion),\n not a ratio-of-sums \u2014 see RATIO.\n\n \"count\" is the DSL spelling of the internal count_events (one per matching\n row); \"count_users\" is unique-by-user. Getting these two confused is the most\n common mistake \u2014 count double-counts a user who fired the event twice.\n\n\nSELECTOR & FILTERS\n------------------\n The selector is the event name plus an optional `{ ... }` filter set that\n narrows which events count. Each filter compares an event LABEL to a quoted\n string:\n\n = equal != not equal\n =~ regex match !~ regex NOT match\n\n - Values are ALWAYS double-quoted strings, even for numeric labels \u2014 they are\n coerced on the server (`amount=~\"^1[0-9]$\"`, `status=\"200\"`).\n - Multiple filters are AND-ed: `{country=\"US\", tier!=\"free\"}`.\n - Every filter label AND the value label must be a label DECLARED on the source\n event. Undeclared labels are a validation error, not a silent no-match.\n\n\nGROUP-BY\n--------\n Splits the result into one series per label combination.\n by (a, b) one series per distinct (a, b) \u2014 keep only these labels\n without (a, b) group by all declared labels EXCEPT a, b\n\n NOTE: group-by is a DASHBOARD-only feature. In an experiment the group-by is\n ignored \u2014 a per-user reducer is applied across the whole arm instead. Group-by\n is NOT allowed on a `ratio` query.\n\n\nRATIO (success rate, conversion, failure %, \u2026)\n-----------------------------------------------\n `ratio(numerator, denominator)` divides one count-style arm by another. Each\n arm is an independent selector with its OWN event and filters, so the two arms\n can be different events:\n\n ratio(count(checkout_completed), count(checkout_started))\n ratio(count_users(paid), count_users(signed_up))\n ratio(count(payment{ok=\"1\"}), count(payment)) one event, filtered arm\n\n Rules:\n - Arms are limited to `count` and `count_users` \u2014 no sum/avg/quantile arm.\n - `count` arms divide event counts; `count_users` arms divide distinct-user\n counts. Pick one shape and use it on both arms unless you mean to mix them.\n - A zero denominator yields 0, not an error.\n - No `by (...)` / `without (...)`, and no retention arm, on a ratio.\n - It is a COHORT RATE over the window \u2014 numerator events \u00F7 denominator events \u2014\n NOT a per-id join. It never matches an individual attempt to its outcome by a\n correlation id; it counts each side over the window and divides. If you need a\n matched pair, resolve the outcome in your own code and emit ONE event carrying\n the result (e.g. an `ok` label), then use `ratio(count(e{ok=\"1\"}), count(e))`\n or `avg(e, ok)`.\n - In an experiment a per-user ratio is a 0/1 outcome \u2014 did the numerator happen,\n among denominator-eligible users \u2014 i.e. a proportion, not a ratio-of-sums.\n - When creating with `--event-name`, pass the NUMERATOR's event name.\n\n\nNOT SUPPORTED (will fail to parse)\n----------------------------------\n - general arithmetic / free-form formulas: `count(a) + count(b)`, `2 * sum(x)`,\n `avg(a) / avg(b)` \u2014 the ONLY division form is the fixed binary\n `ratio(arm, arm)` with count-style arms (see RATIO)\n - a ratio arm that isn't `count` / `count_users`: `ratio(sum(a, v), count(b))`\n - more than two events in one query\n - `by (...)` / `without (...)` on a ratio\n - arbitrary quantiles (only the fixed p50/p75/p90/p95/p99/p999)\n - unquoted or numeric filter values: `{status=200}` \u2192 must be `\"200\"`\n - trailing tokens after the query\n\n\nEXAMPLES (query \u2014 what it measures)\n--------------------------------------\n count_users(checkout_completed)\n distinct users who completed checkout.\n\n count(add_to_cart{country=\"US\"})\n number of US add-to-cart events (a user can count many times).\n\n sum(purchase{country=\"US\"}, amount)\n total purchase amount from US purchase events.\n\n avg(req_dur{tier!=\"free\"}, ms) without (region)\n mean request duration for non-free tiers, one series per region-excluded\n label combination.\n\n p99(req_dur{route=~\"/api/.*\"}, ms) by (route, status)\n 99th-percentile latency of /api/* requests, split by route and status.\n\n unique(login{method=\"sso\"}, device_id)\n distinct SSO devices seen (display-only; an experiment would use per-user avg).\n\n retention_7d(session_start)\n share of users who started a new session within 7 days.\n\n ratio(count(checkout_completed), count(checkout_started))\n checkout conversion rate \u2014 completions \u00F7 starts over the window.\n\n ratio(count(payment{ok=\"0\"}), count(payment))\n payment failure rate from a single event's outcome label.\n";
76
+ declare const METRIC_GRAMMAR = "SHIPEASY METRIC QUERY DSL\n=========================\n\nA metric is an EXPRESSION over an event stream. An aggregation over one event\nis a leaf; leaves compose with `+ - * /`, parentheses group, and a metric may\nend with `by (...)` or `without (...)` to split it into one series per label\ncombination. This DSL is the string you pass to `metrics create --query \"...\"`\n(equivalently the typed `query_ir`).\n\nSo `count(a) / count(b)` is a rate, `count(a) / count(b) * 100` is a\npercentage, and `(count(a) - count(b)) / count(c)` is as ordinary as it looks.\n`ratio(a, b)` still parses and means exactly `a / b`.\n\n\nGRAMMAR (BNF)\n-------------\n Query := Expression GroupBy?\n Expression := Term ((\"+\" | \"-\") Term)*\n Term := Factor ((\"*\" | \"/\") Factor)*\n Factor := \"-\"? Primary\n Primary := Number | \"(\" Expression \")\" | Aggregate | Function\n Selector := Identifier (\"{\" Predicate ((\",\"|\"and\"|\"or\") Predicate)* \",\"? \"}\")?\n Predicate := \"not\"? ( Identifier (\"=\" | \"=~\") StringLiteral\n | Identifier (\">\" | \">=\" | \"<\" | \"<=\") Number\n | Identifier \"not\"? \"in\" \"(\" StringLiteral (\",\" ...)* \")\"\n | Identifier \":\" \"*\"\n | \"(\" Predicate \")\" )\n GroupBy := (\"by\" | \"without\") \"(\" Identifier (\",\" Identifier)* \",\"? \")\"\n StringLiteral := double-quoted; \\\" is a literal quote, \\\\ is a backslash\n Identifier := [A-Za-z_][A-Za-z0-9_]* (case-sensitive)\n\n There is ONE negation and it goes in front of a predicate: `not status=\"ok\"`.\n `!=` and `!~` are refused by name.\n\n The trailing Identifier after the selector \u2014 \"(\" Selector \",\" Identifier \")\" \u2014\n is the VALUE LABEL: the numeric event label the aggregation reduces. Whether it\n is allowed, and whether it is needed, is per aggregation (see below).\n\n<aggregate> ::=\n { count ( <selector> )\n | sum ( <selector> [ , <value-label> ] )\n | avg ( <selector> [ , <value-label> ] )\n | min ( <selector> [ , <value-label> ] )\n | max ( <selector> [ , <value-label> ] )\n | stddev ( <selector> [ , <value-label> ] )\n | p ( <percentile> , <selector> [ , <value-label> ] )\n }\n\n\nAGGREGATIONS\n------------\n| Aggregation | Measures | Value label |\n| --- | --- | --- |\n| `count` | how many matching events happened | not allowed |\n| `sum` | total of a numeric label | optional \u2014 defaults to the event's own value |\n| `avg` | mean of a numeric label | optional \u2014 defaults to the event's own value |\n| `min` | smallest value of a numeric label | optional \u2014 defaults to the event's own value |\n| `max` | largest value of a numeric label | optional \u2014 defaults to the event's own value |\n| `stddev` | spread of a numeric label around its mean | optional \u2014 defaults to the event's own value |\n| `p` | a percentile of a numeric label \u2014 `p(95, req, ms)` | optional \u2014 defaults to the event's own value |\n\n A value label is optional wherever it is allowed: omitted, the aggregation\n reduces the event's own default numeric value. `count` takes the event alone.\n\n `count` is one per matching ROW, so a user who fired the event twice counts\n twice.\n\n In an experiment, `min`, `max` and `p` collapse to a per-user `avg`: the\n t-test needs a per-user mean and variance, and those three have neither. They\n are exact on a dashboard. For an exact experiment metric pick count / sum /\n avg, or a rate \u2014 a rate collapses to a per-user 0/1 outcome (a proportion),\n not a ratio-of-sums.\n\n\nSELECTOR & FILTERS\n------------------\n The selector is the event name plus an optional `{ ... }` filter set that\n narrows which events count. Each filter compares an event LABEL to a quoted\n string:\n\n = equal =~ glob match\n > >= < <= order, NUMERIC labels only, value written BARE\n in (\"a\", \"b\") a value set label:* the label is set at all\n not <predicate> negation, in FRONT of the predicate\n\n There is no `!=` and no `!~`. One negation, one place it goes:\n `{not tier=\"free\"}`. Predicates join with `and` / `or` (a comma is another\n `and`) and parenthesise.\n\n - `=~` takes a GLOB, NOT a regex. Two wildcards, and a glob matches the\n WHOLE value \u2014 there are no anchors because there is nothing to anchor:\n\n * any sequence of characters ? exactly one character\n\n route=~\"/api*\" starts with /api\n route=~\"*checkout\" ends with checkout\n route=~\"*admin*\" contains admin\n route=~\"/v?/users\" /v1/users, /v2/users \u2026\n route=~\"/health\" EXACTLY /health \u2014 same as `route=\"/health\"`\n\n Every other character is a LITERAL, dots included, so `host=~\"*.acme.com\"`\n works as written. Regex syntax \u2014 `^` `$` `.*` `|` `[]` `()` `+` `{}` `\\\\` \u2014 is\n REJECTED at save time rather than reinterpreted: `^/api` would be a glob for\n a value that literally starts with a caret, and `/api/.*` would match\n `/api/.x` but not `/api/x`. Write `/api*` instead.\n - Values are ALWAYS double-quoted strings, even for numeric labels \u2014 they are\n coerced on the server (`status=\"200\"`) \u2014 EXCEPT the order comparisons,\n whose value is written bare (`ms > 500`). A glob on a numeric or boolean\n label is a validation error.\n - Multiple predicates are AND-ed: `{country=\"US\", not tier=\"free\"}`.\n - Every filter label AND the value label must be a label DECLARED on the source\n event. Undeclared labels are a validation error, not a silent no-match.\n\n\nGROUP-BY\n--------\n Splits the result into one series per label combination.\n by (a, b) one series per distinct (a, b) \u2014 keep only these labels\n without (a, b) group by all declared labels EXCEPT a, b\n\n NOTE: group-by is a DASHBOARD-only feature. In an experiment the group-by is\n ignored \u2014 a per-user reducer is applied across the whole arm instead.\n\n\nRATES (success rate, conversion, failure %, \u2026)\n----------------------------------------------\n Division is ordinary: `a / b`. Each side is its own expression with its own\n event and filters, so the two sides can be different events. `ratio(a, b)` is\n the same thing spelled as a function.\n\n count(checkout_completed) / count(checkout_started)\n count(paid) / count(signed_up)\n count(payment{ok=\"1\"}) / count(payment) one event, filtered side\n count(errors) / count(requests) * 100 as a percentage\n\n Rules:\n - A zero denominator yields 0, not an error.\n - `by (...)` / `without (...)` applies to the whole rate, and works.\n - AS AN EXPERIMENT METRIC both sides must be `count`. That is the one real\n restriction, and it is about the per-user collapse rather than the query:\n a rate becomes \"did the numerator happen, among the users eligible for the\n denominator\", which is a 0/1 outcome per user. `sum(a, v) / count(b)`\n charts fine and is refused when attached to an experiment.\n - It is a COHORT RATE over the window \u2014 numerator events \u00F7 denominator events \u2014\n NOT a per-id join. It never matches an individual attempt to its outcome by a\n correlation id; it counts each side over the window and divides. If you need a\n matched pair, resolve the outcome in your own code and emit ONE event carrying\n the result (e.g. an `ok` label), then use `ratio(count(e{ok=\"1\"}), count(e))`\n or `avg(e, ok)`.\n - In an experiment a per-user ratio is a 0/1 outcome \u2014 did the numerator happen,\n among denominator-eligible users \u2014 i.e. a proportion, not a ratio-of-sums.\n - When creating with `--event-name`, pass the NUMERATOR's event name.\n\n\nREFUSED BY NAME\n---------------\n Each of these is recognised in order to be refused, with a message saying what\n to write instead. They are not gaps.\n\n| Spelling | Why it is refused |\n| --- | --- |\n| `count_users(\u2026)` | removed \u2014 Analytics Engine samples rows and weights the survivors, and a distinct count cannot be reweighted (sampling reweights rows, not sets), so it under-reports by the sample interval. Use count() |\n| `unique(\u2026)` | removed from the language \u2014 count(DISTINCT \u2026) takes no sample weight, has no conditional form and cannot merge across groups. Stored metrics using it still render; no new one can be authored. Use count() |\n| `retention_Nd(\u2026)` | has no time-series form; retention lives in the experiment analyzer |\n| `anomalies(\u2026)` | detection is a property of the alert rule now, not of the metric: create an alert with kind 'anomaly' and set its sigma. The metric stays what it measures, and the same seasonal baseline can be drawn on its chart by turning the band on |\n| `deviation(\u2026)` | detection is a property of the alert rule now, not of the metric: an alert with kind 'anomaly' compares this metric against its own seasonal baseline in sigmas, at a sigma and a direction you set on the rule \u2014 so one metric can back rules of different sensitivity |\n| `outliers(\u2026)` | detection is a property of the alert rule now, not of the metric: an alert with kind 'outliers' on a metric grouped by() compares each group against its peers in the same bucket, at a sigma you set on the rule |\n| `expected(\u2026)` | the seasonal band is a display property of the metric now, not a function: turn the band on and every chart of this metric draws it \u2014 at the same sigma an anomaly alert would fire at |\n| `forecast(\u2026)` | the projection is a display property of the metric now, not a function: set the metric's forecast method and horizon, and every chart of it projects forward without changing what the metric measures |\n\n Also refused: `!=` and `!~` (write `not label=\"v\"`), a regex inside `=~`\n (globs only), an order comparison on a text label, a quoted comparison value\n (`ms > \"500\"`), and trailing tokens after the query.\n\n\nEXAMPLES (query \u2014 what it measures)\n--------------------------------------\n count(checkout_completed)\n number of completed checkouts.\n\n count(add_to_cart{country=\"US\"})\n number of US add-to-cart events (a user can count many times).\n\n sum(purchase{country=\"US\"}, amount)\n total purchase amount from US purchase events.\n\n avg(req_dur{not tier=\"free\"}, ms) without (region)\n mean request duration for non-free tiers, one series per region-excluded\n label combination.\n\n p99(req_dur{route=~\"/api*\"}, ms) by (route, status)\n 99th-percentile latency of /api requests, split by route and status.\n\n ratio(count(checkout_completed), count(checkout_started))\n checkout conversion rate \u2014 completions \u00F7 starts over the window.\n\n ratio(count(payment{ok=\"0\"}), count(payment))\n payment failure rate from a single event's outcome label.\n";
77
77
 
78
78
  /**
79
79
  * Every custom (non-spec) operation, in surface order. The CLI and MCP each
package/dist/custom.js CHANGED
@@ -1,65 +1,90 @@
1
+ // src/custom/grammar.generated.ts
2
+ var AGGREGATES_BLOCK = `<aggregate> ::=
3
+ { count ( <selector> )
4
+ | sum ( <selector> [ , <value-label> ] )
5
+ | avg ( <selector> [ , <value-label> ] )
6
+ | min ( <selector> [ , <value-label> ] )
7
+ | max ( <selector> [ , <value-label> ] )
8
+ | stddev ( <selector> [ , <value-label> ] )
9
+ | p ( <percentile> , <selector> [ , <value-label> ] )
10
+ }`;
11
+ var AGGREGATIONS_BLOCK = `| Aggregation | Measures | Value label |
12
+ | --- | --- | --- |
13
+ | \`count\` | how many matching events happened | not allowed |
14
+ | \`sum\` | total of a numeric label | optional \u2014 defaults to the event's own value |
15
+ | \`avg\` | mean of a numeric label | optional \u2014 defaults to the event's own value |
16
+ | \`min\` | smallest value of a numeric label | optional \u2014 defaults to the event's own value |
17
+ | \`max\` | largest value of a numeric label | optional \u2014 defaults to the event's own value |
18
+ | \`stddev\` | spread of a numeric label around its mean | optional \u2014 defaults to the event's own value |
19
+ | \`p\` | a percentile of a numeric label \u2014 \`p(95, req, ms)\` | optional \u2014 defaults to the event's own value |`;
20
+ var REFUSALS_BLOCK = `| Spelling | Why it is refused |
21
+ | --- | --- |
22
+ | \`count_users(\u2026)\` | removed \u2014 Analytics Engine samples rows and weights the survivors, and a distinct count cannot be reweighted (sampling reweights rows, not sets), so it under-reports by the sample interval. Use count() |
23
+ | \`unique(\u2026)\` | removed from the language \u2014 count(DISTINCT \u2026) takes no sample weight, has no conditional form and cannot merge across groups. Stored metrics using it still render; no new one can be authored. Use count() |
24
+ | \`retention_Nd(\u2026)\` | has no time-series form; retention lives in the experiment analyzer |
25
+ | \`anomalies(\u2026)\` | detection is a property of the alert rule now, not of the metric: create an alert with kind 'anomaly' and set its sigma. The metric stays what it measures, and the same seasonal baseline can be drawn on its chart by turning the band on |
26
+ | \`deviation(\u2026)\` | detection is a property of the alert rule now, not of the metric: an alert with kind 'anomaly' compares this metric against its own seasonal baseline in sigmas, at a sigma and a direction you set on the rule \u2014 so one metric can back rules of different sensitivity |
27
+ | \`outliers(\u2026)\` | detection is a property of the alert rule now, not of the metric: an alert with kind 'outliers' on a metric grouped by() compares each group against its peers in the same bucket, at a sigma you set on the rule |
28
+ | \`expected(\u2026)\` | the seasonal band is a display property of the metric now, not a function: turn the band on and every chart of this metric draws it \u2014 at the same sigma an anomaly alert would fire at |
29
+ | \`forecast(\u2026)\` | the projection is a display property of the metric now, not a function: set the metric's forecast method and horizon, and every chart of it projects forward without changing what the metric measures |`;
30
+
1
31
  // src/custom/grammar.ts
2
32
  var METRIC_GRAMMAR = `SHIPEASY METRIC QUERY DSL
3
33
  =========================
4
34
 
5
- A metric is an aggregation over an event stream. Almost always that is ONE
6
- aggregation over ONE event; the single exception is the binary \`ratio(...)\`
7
- form, which divides one count-style arm by another (each arm its own event and
8
- filters) in one pass. This DSL is the string you pass to
9
- \`metrics create --query "..."\` (equivalently the typed \`query_ir\`). There is
10
- no general arithmetic, no free-form formulas, and no subqueries \u2014 \`ratio\` is
11
- the only division / two-event form.
35
+ A metric is an EXPRESSION over an event stream. An aggregation over one event
36
+ is a leaf; leaves compose with \`+ - * /\`, parentheses group, and a metric may
37
+ end with \`by (...)\` or \`without (...)\` to split it into one series per label
38
+ combination. This DSL is the string you pass to \`metrics create --query "..."\`
39
+ (equivalently the typed \`query_ir\`).
40
+
41
+ So \`count(a) / count(b)\` is a rate, \`count(a) / count(b) * 100\` is a
42
+ percentage, and \`(count(a) - count(b)) / count(c)\` is as ordinary as it looks.
43
+ \`ratio(a, b)\` still parses and means exactly \`a / b\`.
12
44
 
13
45
 
14
46
  GRAMMAR (BNF)
15
47
  -------------
16
- Query := Agg | Ratio
17
- Agg := AggFunc "(" Selector ("," Identifier)? ")" GroupBy?
18
- Ratio := "ratio" "(" RatioArm "," RatioArm ")"
19
- RatioArm := ("count_users" | "count") "(" Selector ")"
20
- AggFunc := "count_users" | "count" | "sum" | "avg" | "min" | "max"
21
- | "unique" | "p50" | "p75" | "p90" | "p95" | "p99" | "p999"
22
- | "retention_" <N> "d" (N = 1..90)
23
- Selector := Identifier ("{" Filter ("," Filter)* ","? "}")?
24
- Filter := Identifier MatchOp StringLiteral
25
- MatchOp := "=" | "!=" | "=~" | "!~"
48
+ Query := Expression GroupBy?
49
+ Expression := Term (("+" | "-") Term)*
50
+ Term := Factor (("*" | "/") Factor)*
51
+ Factor := "-"? Primary
52
+ Primary := Number | "(" Expression ")" | Aggregate | Function
53
+ Selector := Identifier ("{" Predicate ((","|"and"|"or") Predicate)* ","? "}")?
54
+ Predicate := "not"? ( Identifier ("=" | "=~") StringLiteral
55
+ | Identifier (">" | ">=" | "<" | "<=") Number
56
+ | Identifier "not"? "in" "(" StringLiteral ("," ...)* ")"
57
+ | Identifier ":" "*"
58
+ | "(" Predicate ")" )
26
59
  GroupBy := ("by" | "without") "(" Identifier ("," Identifier)* ","? ")"
27
60
  StringLiteral := double-quoted; \\" is a literal quote, \\\\ is a backslash
28
61
  Identifier := [A-Za-z_][A-Za-z0-9_]* (case-sensitive)
29
62
 
63
+ There is ONE negation and it goes in front of a predicate: \`not status="ok"\`.
64
+ \`!=\` and \`!~\` are refused by name.
65
+
30
66
  The trailing Identifier after the selector \u2014 "(" Selector "," Identifier ")" \u2014
31
67
  is the VALUE LABEL: the numeric event label the aggregation reduces. Whether it
32
- is required, optional, or forbidden depends on the aggregation (see below).
68
+ is allowed, and whether it is needed, is per aggregation (see below).
69
+
70
+ ${AGGREGATES_BLOCK}
33
71
 
34
72
 
35
73
  AGGREGATIONS
36
74
  ------------
37
- Function Value label Meaning In experiments
38
- -------------- ------------ ------------------------------ ---------------
39
- count_users forbidden distinct users who fired it exact
40
- count forbidden number of events (rows) exact
41
- sum(e, v) required \u03A3 of numeric label v exact
42
- avg(e, v) required mean of label v exact
43
- retention_Nd(e) forbidden % of users who returned exact
44
- within N days (N = 1..90)
45
- min(e, v) required minimum of label v approx \u2192 avg *
46
- max(e, v) required maximum of label v approx \u2192 avg *
47
- unique(e, v) required distinct values of label v approx \u2192 avg *
48
- p50 \u2026 p999(e,v) required quantile of v (50/75/90/ approx \u2192 avg *
49
- 95/99/99.9th percentile)
50
- ratio(a, b) forbidden a \xF7 b (see RATIO below) proportion **
51
-
52
- * Display-only aggregations. The experiment t-test needs a per-user mean and
53
- variance, so min/max/unique/quantile are computed exactly on dashboards but
54
- collapse to a per-user \`avg\` when used as an experiment metric. If you need
55
- an exact experiment metric, pick count_users / count / sum / avg / retention.
56
-
57
- ** In an experiment a ratio collapses to a per-user 0/1 outcome (a proportion),
58
- not a ratio-of-sums \u2014 see RATIO.
59
-
60
- "count" is the DSL spelling of the internal count_events (one per matching
61
- row); "count_users" is unique-by-user. Getting these two confused is the most
62
- common mistake \u2014 count double-counts a user who fired the event twice.
75
+ ${AGGREGATIONS_BLOCK}
76
+
77
+ A value label is optional wherever it is allowed: omitted, the aggregation
78
+ reduces the event's own default numeric value. \`count\` takes the event alone.
79
+
80
+ \`count\` is one per matching ROW, so a user who fired the event twice counts
81
+ twice.
82
+
83
+ In an experiment, \`min\`, \`max\` and \`p\` collapse to a per-user \`avg\`: the
84
+ t-test needs a per-user mean and variance, and those three have neither. They
85
+ are exact on a dashboard. For an exact experiment metric pick count / sum /
86
+ avg, or a rate \u2014 a rate collapses to a per-user 0/1 outcome (a proportion),
87
+ not a ratio-of-sums.
63
88
 
64
89
 
65
90
  SELECTOR & FILTERS
@@ -68,12 +93,36 @@ SELECTOR & FILTERS
68
93
  narrows which events count. Each filter compares an event LABEL to a quoted
69
94
  string:
70
95
 
71
- = equal != not equal
72
- =~ regex match !~ regex NOT match
96
+ = equal =~ glob match
97
+ > >= < <= order, NUMERIC labels only, value written BARE
98
+ in ("a", "b") a value set label:* the label is set at all
99
+ not <predicate> negation, in FRONT of the predicate
100
+
101
+ There is no \`!=\` and no \`!~\`. One negation, one place it goes:
102
+ \`{not tier="free"}\`. Predicates join with \`and\` / \`or\` (a comma is another
103
+ \`and\`) and parenthesise.
73
104
 
105
+ - \`=~\` takes a GLOB, NOT a regex. Two wildcards, and a glob matches the
106
+ WHOLE value \u2014 there are no anchors because there is nothing to anchor:
107
+
108
+ * any sequence of characters ? exactly one character
109
+
110
+ route=~"/api*" starts with /api
111
+ route=~"*checkout" ends with checkout
112
+ route=~"*admin*" contains admin
113
+ route=~"/v?/users" /v1/users, /v2/users \u2026
114
+ route=~"/health" EXACTLY /health \u2014 same as \`route="/health"\`
115
+
116
+ Every other character is a LITERAL, dots included, so \`host=~"*.acme.com"\`
117
+ works as written. Regex syntax \u2014 \`^\` \`$\` \`.*\` \`|\` \`[]\` \`()\` \`+\` \`{}\` \`\\\\\` \u2014 is
118
+ REJECTED at save time rather than reinterpreted: \`^/api\` would be a glob for
119
+ a value that literally starts with a caret, and \`/api/.*\` would match
120
+ \`/api/.x\` but not \`/api/x\`. Write \`/api*\` instead.
74
121
  - Values are ALWAYS double-quoted strings, even for numeric labels \u2014 they are
75
- coerced on the server (\`amount=~"^1[0-9]$"\`, \`status="200"\`).
76
- - Multiple filters are AND-ed: \`{country="US", tier!="free"}\`.
122
+ coerced on the server (\`status="200"\`) \u2014 EXCEPT the order comparisons,
123
+ whose value is written bare (\`ms > 500\`). A glob on a numeric or boolean
124
+ label is a validation error.
125
+ - Multiple predicates are AND-ed: \`{country="US", not tier="free"}\`.
77
126
  - Every filter label AND the value label must be a label DECLARED on the source
78
127
  event. Undeclared labels are a validation error, not a silent no-match.
79
128
 
@@ -85,26 +134,28 @@ GROUP-BY
85
134
  without (a, b) group by all declared labels EXCEPT a, b
86
135
 
87
136
  NOTE: group-by is a DASHBOARD-only feature. In an experiment the group-by is
88
- ignored \u2014 a per-user reducer is applied across the whole arm instead. Group-by
89
- is NOT allowed on a \`ratio\` query.
137
+ ignored \u2014 a per-user reducer is applied across the whole arm instead.
90
138
 
91
139
 
92
- RATIO (success rate, conversion, failure %, \u2026)
93
- -----------------------------------------------
94
- \`ratio(numerator, denominator)\` divides one count-style arm by another. Each
95
- arm is an independent selector with its OWN event and filters, so the two arms
96
- can be different events:
140
+ RATES (success rate, conversion, failure %, \u2026)
141
+ ----------------------------------------------
142
+ Division is ordinary: \`a / b\`. Each side is its own expression with its own
143
+ event and filters, so the two sides can be different events. \`ratio(a, b)\` is
144
+ the same thing spelled as a function.
97
145
 
98
- ratio(count(checkout_completed), count(checkout_started))
99
- ratio(count_users(paid), count_users(signed_up))
100
- ratio(count(payment{ok="1"}), count(payment)) one event, filtered arm
146
+ count(checkout_completed) / count(checkout_started)
147
+ count(paid) / count(signed_up)
148
+ count(payment{ok="1"}) / count(payment) one event, filtered side
149
+ count(errors) / count(requests) * 100 as a percentage
101
150
 
102
151
  Rules:
103
- - Arms are limited to \`count\` and \`count_users\` \u2014 no sum/avg/quantile arm.
104
- - \`count\` arms divide event counts; \`count_users\` arms divide distinct-user
105
- counts. Pick one shape and use it on both arms unless you mean to mix them.
106
152
  - A zero denominator yields 0, not an error.
107
- - No \`by (...)\` / \`without (...)\`, and no retention arm, on a ratio.
153
+ - \`by (...)\` / \`without (...)\` applies to the whole rate, and works.
154
+ - AS AN EXPERIMENT METRIC both sides must be \`count\`. That is the one real
155
+ restriction, and it is about the per-user collapse rather than the query:
156
+ a rate becomes "did the numerator happen, among the users eligible for the
157
+ denominator", which is a 0/1 outcome per user. \`sum(a, v) / count(b)\`
158
+ charts fine and is refused when attached to an experiment.
108
159
  - It is a COHORT RATE over the window \u2014 numerator events \xF7 denominator events \u2014
109
160
  NOT a per-id join. It never matches an individual attempt to its outcome by a
110
161
  correlation id; it counts each side over the window and divides. If you need a
@@ -116,23 +167,22 @@ RATIO (success rate, conversion, failure %, \u2026)
116
167
  - When creating with \`--event-name\`, pass the NUMERATOR's event name.
117
168
 
118
169
 
119
- NOT SUPPORTED (will fail to parse)
120
- ----------------------------------
121
- - general arithmetic / free-form formulas: \`count(a) + count(b)\`, \`2 * sum(x)\`,
122
- \`avg(a) / avg(b)\` \u2014 the ONLY division form is the fixed binary
123
- \`ratio(arm, arm)\` with count-style arms (see RATIO)
124
- - a ratio arm that isn't \`count\` / \`count_users\`: \`ratio(sum(a, v), count(b))\`
125
- - more than two events in one query
126
- - \`by (...)\` / \`without (...)\` on a ratio
127
- - arbitrary quantiles (only the fixed p50/p75/p90/p95/p99/p999)
128
- - unquoted or numeric filter values: \`{status=200}\` \u2192 must be \`"200"\`
129
- - trailing tokens after the query
170
+ REFUSED BY NAME
171
+ ---------------
172
+ Each of these is recognised in order to be refused, with a message saying what
173
+ to write instead. They are not gaps.
174
+
175
+ ${REFUSALS_BLOCK}
176
+
177
+ Also refused: \`!=\` and \`!~\` (write \`not label="v"\`), a regex inside \`=~\`
178
+ (globs only), an order comparison on a text label, a quoted comparison value
179
+ (\`ms > "500"\`), and trailing tokens after the query.
130
180
 
131
181
 
132
182
  EXAMPLES (query \u2014 what it measures)
133
183
  --------------------------------------
134
- count_users(checkout_completed)
135
- distinct users who completed checkout.
184
+ count(checkout_completed)
185
+ number of completed checkouts.
136
186
 
137
187
  count(add_to_cart{country="US"})
138
188
  number of US add-to-cart events (a user can count many times).
@@ -140,18 +190,12 @@ EXAMPLES (query \u2014 what it measures)
140
190
  sum(purchase{country="US"}, amount)
141
191
  total purchase amount from US purchase events.
142
192
 
143
- avg(req_dur{tier!="free"}, ms) without (region)
193
+ avg(req_dur{not tier="free"}, ms) without (region)
144
194
  mean request duration for non-free tiers, one series per region-excluded
145
195
  label combination.
146
196
 
147
- p99(req_dur{route=~"/api/.*"}, ms) by (route, status)
148
- 99th-percentile latency of /api/* requests, split by route and status.
149
-
150
- unique(login{method="sso"}, device_id)
151
- distinct SSO devices seen (display-only; an experiment would use per-user avg).
152
-
153
- retention_7d(session_start)
154
- share of users who started a new session within 7 days.
197
+ p99(req_dur{route=~"/api*"}, ms) by (route, status)
198
+ 99th-percentile latency of /api requests, split by route and status.
155
199
 
156
200
  ratio(count(checkout_completed), count(checkout_started))
157
201
  checkout conversion rate \u2014 completions \xF7 starts over the window.