@adonis-agora/flags 0.0.0-stage → 0.1.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 (322) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +15 -2
  4. package/dist/assets/spa/assets/index-DdmFo7xr.js +12 -0
  5. package/dist/assets/spa/assets/index-DdmFo7xr.js.map +1 -0
  6. package/dist/assets/spa/assets/index-DhkcP3Cw.css +2 -0
  7. package/dist/assets/spa/index.html +26 -0
  8. package/dist/commands/flags_list.d.ts +9 -0
  9. package/dist/commands/flags_list.d.ts.map +1 -0
  10. package/dist/commands/flags_list.js +51 -0
  11. package/dist/commands/flags_list.js.map +1 -0
  12. package/dist/commands/flags_ops.d.ts +60 -0
  13. package/dist/commands/flags_ops.d.ts.map +1 -0
  14. package/dist/commands/flags_ops.js +253 -0
  15. package/dist/commands/flags_ops.js.map +1 -0
  16. package/dist/commands/flags_refs.d.ts +15 -0
  17. package/dist/commands/flags_refs.d.ts.map +1 -0
  18. package/dist/commands/flags_refs.js +90 -0
  19. package/dist/commands/flags_refs.js.map +1 -0
  20. package/dist/commands/flags_set.d.ts +19 -0
  21. package/dist/commands/flags_set.d.ts.map +1 -0
  22. package/dist/commands/flags_set.js +97 -0
  23. package/dist/commands/flags_set.js.map +1 -0
  24. package/dist/commands/flags_types.d.ts +18 -0
  25. package/dist/commands/flags_types.d.ts.map +1 -0
  26. package/dist/commands/flags_types.js +94 -0
  27. package/dist/commands/flags_types.js.map +1 -0
  28. package/dist/commands/flags_work.d.ts +16 -0
  29. package/dist/commands/flags_work.d.ts.map +1 -0
  30. package/dist/commands/flags_work.js +71 -0
  31. package/dist/commands/flags_work.js.map +1 -0
  32. package/dist/commands/main.d.ts +15 -0
  33. package/dist/commands/main.d.ts.map +1 -0
  34. package/dist/commands/main.js +30 -0
  35. package/dist/commands/main.js.map +1 -0
  36. package/dist/commands/shared.d.ts +21 -0
  37. package/dist/commands/shared.d.ts.map +1 -0
  38. package/dist/commands/shared.js +70 -0
  39. package/dist/commands/shared.js.map +1 -0
  40. package/dist/configure.d.ts +11 -0
  41. package/dist/configure.d.ts.map +1 -0
  42. package/dist/configure.js +32 -0
  43. package/dist/configure.js.map +1 -0
  44. package/dist/providers/dashboard_provider.d.ts +65 -0
  45. package/dist/providers/dashboard_provider.d.ts.map +1 -0
  46. package/dist/providers/dashboard_provider.js +340 -0
  47. package/dist/providers/dashboard_provider.js.map +1 -0
  48. package/dist/providers/flags_provider.d.ts +30 -0
  49. package/dist/providers/flags_provider.d.ts.map +1 -0
  50. package/dist/providers/flags_provider.js +184 -0
  51. package/dist/providers/flags_provider.js.map +1 -0
  52. package/dist/services/main.d.ts +17 -0
  53. package/dist/services/main.d.ts.map +1 -0
  54. package/dist/services/main.js +20 -0
  55. package/dist/services/main.js.map +1 -0
  56. package/dist/src/admin.d.ts +189 -0
  57. package/dist/src/admin.d.ts.map +1 -0
  58. package/dist/src/admin.js +539 -0
  59. package/dist/src/admin.js.map +1 -0
  60. package/dist/src/analytics/backend.d.ts +18 -0
  61. package/dist/src/analytics/backend.d.ts.map +1 -0
  62. package/dist/src/analytics/backend.js +25 -0
  63. package/dist/src/analytics/backend.js.map +1 -0
  64. package/dist/src/analytics/clickhouse_backend.d.ts +16 -0
  65. package/dist/src/analytics/clickhouse_backend.d.ts.map +1 -0
  66. package/dist/src/analytics/clickhouse_backend.js +48 -0
  67. package/dist/src/analytics/clickhouse_backend.js.map +1 -0
  68. package/dist/src/analytics/duckdb.d.ts +39 -0
  69. package/dist/src/analytics/duckdb.d.ts.map +1 -0
  70. package/dist/src/analytics/duckdb.js +117 -0
  71. package/dist/src/analytics/duckdb.js.map +1 -0
  72. package/dist/src/analytics/lucid_backend.d.ts +16 -0
  73. package/dist/src/analytics/lucid_backend.d.ts.map +1 -0
  74. package/dist/src/analytics/lucid_backend.js +45 -0
  75. package/dist/src/analytics/lucid_backend.js.map +1 -0
  76. package/dist/src/analytics/metrics.d.ts +50 -0
  77. package/dist/src/analytics/metrics.d.ts.map +1 -0
  78. package/dist/src/analytics/metrics.js +42 -0
  79. package/dist/src/analytics/metrics.js.map +1 -0
  80. package/dist/src/analytics/results.d.ts +80 -0
  81. package/dist/src/analytics/results.d.ts.map +1 -0
  82. package/dist/src/analytics/results.js +143 -0
  83. package/dist/src/analytics/results.js.map +1 -0
  84. package/dist/src/dashboard/access_denied_page.d.ts +100 -0
  85. package/dist/src/dashboard/access_denied_page.d.ts.map +1 -0
  86. package/dist/src/dashboard/access_denied_page.js +262 -0
  87. package/dist/src/dashboard/access_denied_page.js.map +1 -0
  88. package/dist/src/dashboard/actor.d.ts +12 -0
  89. package/dist/src/dashboard/actor.d.ts.map +1 -0
  90. package/dist/src/dashboard/actor.js +33 -0
  91. package/dist/src/dashboard/actor.js.map +1 -0
  92. package/dist/src/dashboard/authorize_decision.d.ts +48 -0
  93. package/dist/src/dashboard/authorize_decision.d.ts.map +1 -0
  94. package/dist/src/dashboard/authorize_decision.js +44 -0
  95. package/dist/src/dashboard/authorize_decision.js.map +1 -0
  96. package/dist/src/dashboard/console_authorize.d.ts +77 -0
  97. package/dist/src/dashboard/console_authorize.d.ts.map +1 -0
  98. package/dist/src/dashboard/console_authorize.js +104 -0
  99. package/dist/src/dashboard/console_authorize.js.map +1 -0
  100. package/dist/src/dashboard/define_config.d.ts +71 -0
  101. package/dist/src/dashboard/define_config.d.ts.map +1 -0
  102. package/dist/src/dashboard/define_config.js +18 -0
  103. package/dist/src/dashboard/define_config.js.map +1 -0
  104. package/dist/src/dashboard/handlers.d.ts +172 -0
  105. package/dist/src/dashboard/handlers.d.ts.map +1 -0
  106. package/dist/src/dashboard/handlers.js +949 -0
  107. package/dist/src/dashboard/handlers.js.map +1 -0
  108. package/dist/src/dashboard/index.d.ts +15 -0
  109. package/dist/src/dashboard/index.d.ts.map +1 -0
  110. package/dist/src/dashboard/index.js +10 -0
  111. package/dist/src/dashboard/index.js.map +1 -0
  112. package/dist/src/dashboard/shape.d.ts +12 -0
  113. package/dist/src/dashboard/shape.d.ts.map +1 -0
  114. package/dist/src/dashboard/shape.js +232 -0
  115. package/dist/src/dashboard/shape.js.map +1 -0
  116. package/dist/src/dashboard/spa.d.ts +34 -0
  117. package/dist/src/dashboard/spa.d.ts.map +1 -0
  118. package/dist/src/dashboard/spa.js +50 -0
  119. package/dist/src/dashboard/spa.js.map +1 -0
  120. package/dist/src/declare.d.ts +85 -0
  121. package/dist/src/declare.d.ts.map +1 -0
  122. package/dist/src/declare.js +62 -0
  123. package/dist/src/declare.js.map +1 -0
  124. package/dist/src/define_config.d.ts +256 -0
  125. package/dist/src/define_config.d.ts.map +1 -0
  126. package/dist/src/define_config.js +90 -0
  127. package/dist/src/define_config.js.map +1 -0
  128. package/dist/src/durable.d.ts +29 -0
  129. package/dist/src/durable.d.ts.map +1 -0
  130. package/dist/src/durable.js +55 -0
  131. package/dist/src/durable.js.map +1 -0
  132. package/dist/src/enrichers.d.ts +51 -0
  133. package/dist/src/enrichers.d.ts.map +1 -0
  134. package/dist/src/enrichers.js +105 -0
  135. package/dist/src/enrichers.js.map +1 -0
  136. package/dist/src/events/buffer.d.ts +38 -0
  137. package/dist/src/events/buffer.d.ts.map +1 -0
  138. package/dist/src/events/buffer.js +86 -0
  139. package/dist/src/events/buffer.js.map +1 -0
  140. package/dist/src/events/event.d.ts +42 -0
  141. package/dist/src/events/event.d.ts.map +1 -0
  142. package/dist/src/events/event.js +82 -0
  143. package/dist/src/events/event.js.map +1 -0
  144. package/dist/src/http/browser.d.ts +33 -0
  145. package/dist/src/http/browser.d.ts.map +1 -0
  146. package/dist/src/http/browser.js +81 -0
  147. package/dist/src/http/browser.js.map +1 -0
  148. package/dist/src/http/browser_api.d.ts +48 -0
  149. package/dist/src/http/browser_api.d.ts.map +1 -0
  150. package/dist/src/http/browser_api.js +273 -0
  151. package/dist/src/http/browser_api.js.map +1 -0
  152. package/dist/src/http/browser_tokens.d.ts +27 -0
  153. package/dist/src/http/browser_tokens.d.ts.map +1 -0
  154. package/dist/src/http/browser_tokens.js +48 -0
  155. package/dist/src/http/browser_tokens.js.map +1 -0
  156. package/dist/src/http/control_plane.d.ts +58 -0
  157. package/dist/src/http/control_plane.d.ts.map +1 -0
  158. package/dist/src/http/control_plane.js +318 -0
  159. package/dist/src/http/control_plane.js.map +1 -0
  160. package/dist/src/http/csrf.d.ts +22 -0
  161. package/dist/src/http/csrf.d.ts.map +1 -0
  162. package/dist/src/http/csrf.js +24 -0
  163. package/dist/src/http/csrf.js.map +1 -0
  164. package/dist/src/http/edge_tag.d.ts +55 -0
  165. package/dist/src/http/edge_tag.d.ts.map +1 -0
  166. package/dist/src/http/edge_tag.js +45 -0
  167. package/dist/src/http/edge_tag.js.map +1 -0
  168. package/dist/src/http/flag_middleware.d.ts +27 -0
  169. package/dist/src/http/flag_middleware.d.ts.map +1 -0
  170. package/dist/src/http/flag_middleware.js +30 -0
  171. package/dist/src/http/flag_middleware.js.map +1 -0
  172. package/dist/src/http/flags_middleware.d.ts +20 -0
  173. package/dist/src/http/flags_middleware.d.ts.map +1 -0
  174. package/dist/src/http/flags_middleware.js +20 -0
  175. package/dist/src/http/flags_middleware.js.map +1 -0
  176. package/dist/src/http/subject_resolver.d.ts +45 -0
  177. package/dist/src/http/subject_resolver.d.ts.map +1 -0
  178. package/dist/src/http/subject_resolver.js +93 -0
  179. package/dist/src/http/subject_resolver.js.map +1 -0
  180. package/dist/src/identity.d.ts +82 -0
  181. package/dist/src/identity.d.ts.map +1 -0
  182. package/dist/src/identity.js +100 -0
  183. package/dist/src/identity.js.map +1 -0
  184. package/dist/src/index.d.ts +41 -0
  185. package/dist/src/index.d.ts.map +1 -0
  186. package/dist/src/index.js +34 -0
  187. package/dist/src/index.js.map +1 -0
  188. package/dist/src/manager.d.ts +154 -0
  189. package/dist/src/manager.d.ts.map +1 -0
  190. package/dist/src/manager.js +333 -0
  191. package/dist/src/manager.js.map +1 -0
  192. package/dist/src/openfeature/provider.d.ts +36 -0
  193. package/dist/src/openfeature/provider.d.ts.map +1 -0
  194. package/dist/src/openfeature/provider.js +82 -0
  195. package/dist/src/openfeature/provider.js.map +1 -0
  196. package/dist/src/relay/relay.d.ts +36 -0
  197. package/dist/src/relay/relay.d.ts.map +1 -0
  198. package/dist/src/relay/relay.js +73 -0
  199. package/dist/src/relay/relay.js.map +1 -0
  200. package/dist/src/relay/results_cache.d.ts +20 -0
  201. package/dist/src/relay/results_cache.d.ts.map +1 -0
  202. package/dist/src/relay/results_cache.js +31 -0
  203. package/dist/src/relay/results_cache.js.map +1 -0
  204. package/dist/src/relay/worker.d.ts +66 -0
  205. package/dist/src/relay/worker.d.ts.map +1 -0
  206. package/dist/src/relay/worker.js +177 -0
  207. package/dist/src/relay/worker.js.map +1 -0
  208. package/dist/src/ruleset_source.d.ts +73 -0
  209. package/dist/src/ruleset_source.d.ts.map +1 -0
  210. package/dist/src/ruleset_source.js +128 -0
  211. package/dist/src/ruleset_source.js.map +1 -0
  212. package/dist/src/runtime.d.ts +70 -0
  213. package/dist/src/runtime.d.ts.map +1 -0
  214. package/dist/src/runtime.js +301 -0
  215. package/dist/src/runtime.js.map +1 -0
  216. package/dist/src/schema.d.ts +39 -0
  217. package/dist/src/schema.d.ts.map +1 -0
  218. package/dist/src/schema.js +311 -0
  219. package/dist/src/schema.js.map +1 -0
  220. package/dist/src/services/accessor.d.ts +5 -0
  221. package/dist/src/services/accessor.d.ts.map +1 -0
  222. package/dist/src/services/accessor.js +20 -0
  223. package/dist/src/services/accessor.js.map +1 -0
  224. package/dist/src/sinks/clickhouse.d.ts +41 -0
  225. package/dist/src/sinks/clickhouse.d.ts.map +1 -0
  226. package/dist/src/sinks/clickhouse.js +117 -0
  227. package/dist/src/sinks/clickhouse.js.map +1 -0
  228. package/dist/src/sinks/lucid_sink.d.ts +25 -0
  229. package/dist/src/sinks/lucid_sink.d.ts.map +1 -0
  230. package/dist/src/sinks/lucid_sink.js +87 -0
  231. package/dist/src/sinks/lucid_sink.js.map +1 -0
  232. package/dist/src/sinks/parquet_sink.d.ts +41 -0
  233. package/dist/src/sinks/parquet_sink.d.ts.map +1 -0
  234. package/dist/src/sinks/parquet_sink.js +140 -0
  235. package/dist/src/sinks/parquet_sink.js.map +1 -0
  236. package/dist/src/sinks/posthog_sink.d.ts +29 -0
  237. package/dist/src/sinks/posthog_sink.d.ts.map +1 -0
  238. package/dist/src/sinks/posthog_sink.js +65 -0
  239. package/dist/src/sinks/posthog_sink.js.map +1 -0
  240. package/dist/src/sinks/sink.d.ts +36 -0
  241. package/dist/src/sinks/sink.d.ts.map +1 -0
  242. package/dist/src/sinks/sink.js +36 -0
  243. package/dist/src/sinks/sink.js.map +1 -0
  244. package/dist/src/sql.d.ts +33 -0
  245. package/dist/src/sql.d.ts.map +1 -0
  246. package/dist/src/sql.js +66 -0
  247. package/dist/src/sql.js.map +1 -0
  248. package/dist/src/stores/lucid_store.d.ts +55 -0
  249. package/dist/src/stores/lucid_store.d.ts.map +1 -0
  250. package/dist/src/stores/lucid_store.js +412 -0
  251. package/dist/src/stores/lucid_store.js.map +1 -0
  252. package/dist/src/stores/memory_store.d.ts +38 -0
  253. package/dist/src/stores/memory_store.d.ts.map +1 -0
  254. package/dist/src/stores/memory_store.js +166 -0
  255. package/dist/src/stores/memory_store.js.map +1 -0
  256. package/dist/src/stores/store.d.ts +166 -0
  257. package/dist/src/stores/store.d.ts.map +1 -0
  258. package/dist/src/stores/store.js +67 -0
  259. package/dist/src/stores/store.js.map +1 -0
  260. package/dist/src/subject.d.ts +25 -0
  261. package/dist/src/subject.d.ts.map +1 -0
  262. package/dist/src/subject.js +30 -0
  263. package/dist/src/subject.js.map +1 -0
  264. package/dist/src/testing/index.d.ts +31 -0
  265. package/dist/src/testing/index.d.ts.map +1 -0
  266. package/dist/src/testing/index.js +60 -0
  267. package/dist/src/testing/index.js.map +1 -0
  268. package/dist/src/topology/browser_token.d.ts +48 -0
  269. package/dist/src/topology/browser_token.d.ts.map +1 -0
  270. package/dist/src/topology/browser_token.js +128 -0
  271. package/dist/src/topology/browser_token.js.map +1 -0
  272. package/dist/src/topology/remote_source.d.ts +39 -0
  273. package/dist/src/topology/remote_source.d.ts.map +1 -0
  274. package/dist/src/topology/remote_source.js +184 -0
  275. package/dist/src/topology/remote_source.js.map +1 -0
  276. package/dist/src/topology/token.d.ts +36 -0
  277. package/dist/src/topology/token.d.ts.map +1 -0
  278. package/dist/src/topology/token.js +68 -0
  279. package/dist/src/topology/token.js.map +1 -0
  280. package/dist/src/topology/verify.d.ts +88 -0
  281. package/dist/src/topology/verify.d.ts.map +1 -0
  282. package/dist/src/topology/verify.js +128 -0
  283. package/dist/src/topology/verify.js.map +1 -0
  284. package/dist/src/transports/db_transport.d.ts +55 -0
  285. package/dist/src/transports/db_transport.d.ts.map +1 -0
  286. package/dist/src/transports/db_transport.js +202 -0
  287. package/dist/src/transports/db_transport.js.map +1 -0
  288. package/dist/src/transports/http_transport.d.ts +30 -0
  289. package/dist/src/transports/http_transport.d.ts.map +1 -0
  290. package/dist/src/transports/http_transport.js +51 -0
  291. package/dist/src/transports/http_transport.js.map +1 -0
  292. package/dist/src/transports/memory_transport.d.ts +29 -0
  293. package/dist/src/transports/memory_transport.d.ts.map +1 -0
  294. package/dist/src/transports/memory_transport.js +91 -0
  295. package/dist/src/transports/memory_transport.js.map +1 -0
  296. package/dist/src/transports/redis_transport.d.ts +50 -0
  297. package/dist/src/transports/redis_transport.d.ts.map +1 -0
  298. package/dist/src/transports/redis_transport.js +107 -0
  299. package/dist/src/transports/redis_transport.js.map +1 -0
  300. package/dist/src/transports/transport.d.ts +62 -0
  301. package/dist/src/transports/transport.d.ts.map +1 -0
  302. package/dist/src/transports/transport.js +2 -0
  303. package/dist/src/transports/transport.js.map +1 -0
  304. package/dist/src/types.d.ts +30 -0
  305. package/dist/src/types.d.ts.map +1 -0
  306. package/dist/src/types.js +2 -0
  307. package/dist/src/types.js.map +1 -0
  308. package/dist/src/usage.d.ts +18 -0
  309. package/dist/src/usage.d.ts.map +1 -0
  310. package/dist/src/usage.js +52 -0
  311. package/dist/src/usage.js.map +1 -0
  312. package/dist/stubs/config/flags.stub +50 -0
  313. package/dist/stubs/config/flags_dashboard.stub +35 -0
  314. package/dist/stubs/database/migrations/create_flags_tables.stub +27 -0
  315. package/dist/stubs/main.d.ts +3 -0
  316. package/dist/stubs/main.d.ts.map +1 -0
  317. package/dist/stubs/main.js +9 -0
  318. package/dist/stubs/main.js.map +1 -0
  319. package/package.json +178 -3
  320. package/skills/flags-basics/SKILL.md +142 -0
  321. package/skills/flags-deployment/SKILL.md +125 -0
  322. package/skills/flags-experiments/SKILL.md +99 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clickhouse_backend.d.ts","sourceRoot":"","sources":["../../../src/analytics/clickhouse_backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAClF,OAAO,EAAE,KAAK,gBAAgB,EAAgB,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,KAAK,gBAAgB,EAAqB,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEjG;;;GAGG;AACH,qBAAa,iBAAkB,YAAW,gBAAgB;;IACxD,QAAQ,CAAC,IAAI,gBAAgB;IAC7B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAElC,YAAY,OAAO,EAAE,iBAAiB,EAErC;IAUK,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAMxF;IAEK,MAAM,CACV,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,gBAAgB,EAC5B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAwB7C;CACF"}
@@ -0,0 +1,48 @@
1
+ import { ClickHouseClient } from '../sinks/clickhouse.js';
2
+ import { statsFromRow } from './backend.js';
3
+ import { subjectAggregates } from './metrics.js';
4
+ /**
5
+ * Level 3 of DESIGN §12: reads the table the `clickhouse` sink writes. Queries use `FINAL` so
6
+ * re-sent events are collapsed even before ClickHouse merges them.
7
+ */
8
+ export class ClickHouseBackend {
9
+ kind = 'clickhouse';
10
+ client;
11
+ constructor(options) {
12
+ this.client = new ClickHouseClient(options);
13
+ }
14
+ #exposuresSql() {
15
+ return `SELECT subject, argMin(variant, ts) AS variant, min(ts) AS first_seen_ts
16
+ FROM ${this.client.table} FINAL
17
+ WHERE type = 'exposure' AND environment = {environment:String}
18
+ AND experiment = {experiment:String}
19
+ GROUP BY subject`;
20
+ }
21
+ async exposures(experiment, environment) {
22
+ const rows = await this.client.select(`SELECT variant, count() AS n FROM (${this.#exposuresSql()}) GROUP BY variant`, { experiment, environment });
23
+ return Object.fromEntries(rows.map((row) => [row.variant, Number(row.n)]));
24
+ }
25
+ async metric(experiment, definition, environment) {
26
+ const { post, pre } = subjectAggregates(definition);
27
+ const rows = await this.client.select(`WITH x AS (${this.#exposuresSql()}),
28
+ t AS (
29
+ SELECT subject, value, ts FROM ${this.client.table} FINAL
30
+ WHERE type = 'track' AND environment = {environment:String}
31
+ AND name = {event:String}
32
+ )
33
+ SELECT variant, count() AS n,
34
+ sum(y) AS sum_y, sum(y * y) AS sum_y2,
35
+ sum(xv) AS sum_x, sum(xv * xv) AS sum_x2, sum(xv * y) AS sum_xy
36
+ FROM (
37
+ SELECT x.variant AS variant, x.subject AS subject,
38
+ toFloat64(${post}) AS y,
39
+ toFloat64(${pre}) AS xv
40
+ FROM x
41
+ LEFT JOIN t ON t.subject = x.subject
42
+ GROUP BY x.variant, x.subject
43
+ )
44
+ GROUP BY variant`, { experiment, environment, event: definition.event });
45
+ return Object.fromEntries(rows.map((row) => [String(row.variant), statsFromRow(row)]));
46
+ }
47
+ }
48
+ //# sourceMappingURL=clickhouse_backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clickhouse_backend.js","sourceRoot":"","sources":["../../../src/analytics/clickhouse_backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAA0B,MAAM,wBAAwB,CAAC;AAClF,OAAO,EAAyB,YAAY,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAyB,iBAAiB,EAA2B,MAAM,cAAc,CAAC;AAEjG;;;GAGG;AACH,MAAM,OAAO,iBAAiB;IACnB,IAAI,GAAG,YAAY,CAAC;IACpB,MAAM,CAAmB;IAElC,YAAY,OAA0B;QACpC,IAAI,CAAC,MAAM,GAAG,IAAI,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;IAED,aAAa;QACX,OAAO;qBACU,IAAI,CAAC,MAAM,CAAC,KAAK;;;8BAGR,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,UAAkB,EAAE,WAAmB;QACrD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CACnC,sCAAsC,IAAI,CAAC,aAAa,EAAE,oBAAoB,EAC9E,EAAE,UAAU,EAAE,WAAW,EAAE,CAC5B,CAAC;QACF,OAAO,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7E,CAAC;IAED,KAAK,CAAC,MAAM,CACV,UAAkB,EAClB,UAA4B,EAC5B,WAAmB;QAEnB,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CACnC,cAAc,IAAI,CAAC,aAAa,EAAE;;+CAEO,IAAI,CAAC,MAAM,CAAC,KAAK;;;;;;;;;8BASlC,IAAI;8BACJ,GAAG;;;;;yBAKR,EACnB,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,CACrD,CAAC;QACF,OAAO,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACzF,CAAC;CACF"}
@@ -0,0 +1,39 @@
1
+ import { type AnalyticsBackend } from './backend.js';
2
+ import { type MetricDefinition, type VariantMetricStats } from './metrics.js';
3
+ export interface DuckDbOptions {
4
+ /** DuckDB database file. Default `:memory:` (the data lives in the Parquet files). */
5
+ database?: string;
6
+ /**
7
+ * Statements run on every new connection: `INSTALL httpfs; LOAD httpfs;`, `CREATE SECRET …` for
8
+ * S3/GCS credentials, `SET memory_limit = '1GB'`.
9
+ */
10
+ setup?: string[];
11
+ }
12
+ type Sql = (statement: string) => Promise<void>;
13
+ type Select = <T = Record<string, unknown>>(statement: string) => Promise<T[]>;
14
+ /** A lazily-created DuckDB instance. `@duckdb/node-api` is imported on first use only. */
15
+ export declare class DuckDb {
16
+ #private;
17
+ private readonly options;
18
+ constructor(options?: DuckDbOptions);
19
+ withConnection<T>(fn: (sql: Sql, select: Select) => Promise<T>): Promise<T>;
20
+ close(): Promise<void>;
21
+ }
22
+ /**
23
+ * Level 2 of DESIGN §12: reads the Parquet files the `parquet` sink writes, with DuckDB, in process.
24
+ * No server to run. Point it at the same `path`. Queries scan files, so run it where a few seconds
25
+ * of CPU are fine (the worker computes and caches results; the console reads the cache).
26
+ */
27
+ export declare class DuckDbBackend implements AnalyticsBackend {
28
+ #private;
29
+ private readonly options;
30
+ readonly kind = "duckdb";
31
+ constructor(options: DuckDbOptions & {
32
+ path: string;
33
+ });
34
+ exposures(experiment: string, environment: string): Promise<Record<string, number>>;
35
+ metric(experiment: string, definition: MetricDefinition, environment: string): Promise<Record<string, VariantMetricStats>>;
36
+ close(): Promise<void>;
37
+ }
38
+ export {};
39
+ //# sourceMappingURL=duckdb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duckdb.d.ts","sourceRoot":"","sources":["../../../src/analytics/duckdb.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAgB,MAAM,cAAc,CAAC;AACnE,OAAO,EACL,KAAK,gBAAgB,EAGrB,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,aAAa;IAC5B,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,KAAK,GAAG,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAChD,KAAK,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;AAkB/E,0FAA0F;AAC1F,qBAAa,MAAM;;IAGL,OAAO,CAAC,QAAQ,CAAC,OAAO;IAApC,YAA6B,OAAO,GAAE,aAAkB,EAAI;IAiBtD,cAAc,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAchF;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAG3B;CACF;AAMD;;;;GAIG;AACH,qBAAa,aAAc,YAAW,gBAAgB;;IAIxC,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,QAAQ,CAAC,IAAI,YAAY;IAGzB,YAA6B,OAAO,EAAE,aAAa,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAErE;IAeK,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAOxF;IAEK,MAAM,CACV,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,gBAAgB,EAC5B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CA+B7C;IAED,KAAK,kBAEJ;CACF"}
@@ -0,0 +1,117 @@
1
+ import { statsFromRow } from './backend.js';
2
+ import { subjectAggregates, UNBOUNDED_MS, } from './metrics.js';
3
+ /** A lazily-created DuckDB instance. `@duckdb/node-api` is imported on first use only. */
4
+ export class DuckDb {
5
+ options;
6
+ #instance;
7
+ constructor(options = {}) {
8
+ this.options = options;
9
+ }
10
+ async #get() {
11
+ if (!this.#instance) {
12
+ let mod;
13
+ try {
14
+ mod = (await import('@duckdb/node-api'));
15
+ }
16
+ catch {
17
+ throw new Error('@adonis-agora/flags: the parquet sink and the duckdb analytics backend need "@duckdb/node-api". Install it.');
18
+ }
19
+ this.#instance = await mod.DuckDBInstance.create(this.options.database ?? ':memory:');
20
+ }
21
+ return this.#instance;
22
+ }
23
+ async withConnection(fn) {
24
+ const instance = await this.#get();
25
+ const connection = await instance.connect();
26
+ try {
27
+ for (const statement of this.options.setup ?? [])
28
+ await connection.run(statement);
29
+ const sql = async (statement) => {
30
+ await connection.run(statement);
31
+ };
32
+ const select = async (statement) => (await connection.runAndReadAll(statement)).getRowObjectsJS();
33
+ return await fn(sql, select);
34
+ }
35
+ finally {
36
+ connection.closeSync();
37
+ }
38
+ }
39
+ async close() {
40
+ this.#instance?.closeSync();
41
+ this.#instance = undefined;
42
+ }
43
+ }
44
+ function literal(value) {
45
+ return typeof value === 'number' ? String(value) : `'${value.replaceAll("'", "''")}'`;
46
+ }
47
+ /**
48
+ * Level 2 of DESIGN §12: reads the Parquet files the `parquet` sink writes, with DuckDB, in process.
49
+ * No server to run. Point it at the same `path`. Queries scan files, so run it where a few seconds
50
+ * of CPU are fine (the worker computes and caches results; the console reads the cache).
51
+ */
52
+ export class DuckDbBackend {
53
+ options;
54
+ kind = 'duckdb';
55
+ #duck;
56
+ constructor(options) {
57
+ this.options = options;
58
+ this.#duck = new DuckDb(options);
59
+ }
60
+ get #glob() {
61
+ return literal(`${this.options.path.replace(/\/$/, '')}/**/*.parquet`);
62
+ }
63
+ /** First exposure per subject, deduped by event id like every other backend. */
64
+ #exposuresSql(experiment, environment) {
65
+ return `SELECT subject, arg_min(variant, ts) AS variant, min(ts) AS first_seen_ts
66
+ FROM read_parquet(${this.#glob}, hive_partitioning = true, union_by_name = true)
67
+ WHERE type = 'exposure' AND environment = ${literal(environment)}
68
+ AND experiment = ${literal(experiment)}
69
+ GROUP BY subject`;
70
+ }
71
+ async exposures(experiment, environment) {
72
+ return this.#duck.withConnection(async (_sql, select) => {
73
+ const rows = await select(`SELECT variant, COUNT(*) AS n FROM (${this.#exposuresSql(experiment, environment)}) GROUP BY variant`).catch(noFiles);
74
+ return Object.fromEntries(rows.map((row) => [row.variant, Number(row.n)]));
75
+ });
76
+ }
77
+ async metric(experiment, definition, environment) {
78
+ const { post, pre } = subjectAggregates(definition);
79
+ const window = definition.windowMs ?? UNBOUNDED_MS;
80
+ const preWindow = definition.cupedMs ?? 0;
81
+ return this.#duck.withConnection(async (_sql, select) => {
82
+ const rows = await select(`WITH x AS (${this.#exposuresSql(experiment, environment)}),
83
+ t AS (
84
+ SELECT DISTINCT ON (event_id) subject, value, ts
85
+ FROM read_parquet(${this.#glob}, hive_partitioning = true, union_by_name = true)
86
+ WHERE type = 'track' AND environment = ${literal(environment)}
87
+ AND name = ${literal(definition.event)}
88
+ )
89
+ SELECT variant, COUNT(*) AS n,
90
+ SUM(y) AS sum_y, SUM(y * y) AS sum_y2,
91
+ SUM(xv) AS sum_x, SUM(xv * xv) AS sum_x2, SUM(xv * y) AS sum_xy
92
+ FROM (
93
+ SELECT x.variant AS variant, x.subject AS subject,
94
+ CAST(${post} AS DOUBLE) AS y,
95
+ CAST(${pre} AS DOUBLE) AS xv
96
+ FROM x
97
+ LEFT JOIN t
98
+ ON t.subject = x.subject
99
+ AND t.ts >= x.first_seen_ts - ${preWindow}
100
+ AND t.ts < x.first_seen_ts + ${window}
101
+ GROUP BY x.variant, x.subject
102
+ )
103
+ GROUP BY variant`).catch(noFiles);
104
+ return Object.fromEntries(rows.map((row) => [String(row.variant), statsFromRow(row)]));
105
+ });
106
+ }
107
+ close() {
108
+ return this.#duck.close();
109
+ }
110
+ }
111
+ /** No Parquet file yet means no data, not an error. */
112
+ function noFiles(error) {
113
+ if (/No files found|does not exist|IO Error/i.test(String(error?.message)))
114
+ return [];
115
+ throw error;
116
+ }
117
+ //# sourceMappingURL=duckdb.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duckdb.js","sourceRoot":"","sources":["../../../src/analytics/duckdb.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { type SqlClient } from '../sql.js';
2
+ import { type AnalyticsBackend } from './backend.js';
3
+ import { type MetricDefinition, type VariantMetricStats } from './metrics.js';
4
+ /**
5
+ * Reads the `lucid` sink's tables (`flags_exposures`, `flags_tracks`). Level 1 of DESIGN §12: fine
6
+ * up to tens of millions of events a month, especially with results computed by the worker and
7
+ * cached, as the provider does.
8
+ */
9
+ export declare class LucidBackend implements AnalyticsBackend {
10
+ private readonly db;
11
+ readonly kind = "lucid";
12
+ constructor(db: SqlClient);
13
+ exposures(experiment: string, environment: string): Promise<Record<string, number>>;
14
+ metric(experiment: string, definition: MetricDefinition, environment: string): Promise<Record<string, VariantMetricStats>>;
15
+ }
16
+ //# sourceMappingURL=lucid_backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lucid_backend.d.ts","sourceRoot":"","sources":["../../../src/analytics/lucid_backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,SAAS,EAAY,MAAM,WAAW,CAAC;AAC5D,OAAO,EAAE,KAAK,gBAAgB,EAAgB,MAAM,cAAc,CAAC;AACnE,OAAO,EACL,KAAK,gBAAgB,EAGrB,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAC;AAEtB;;;;GAIG;AACH,qBAAa,YAAa,YAAW,gBAAgB;IAGvC,OAAO,CAAC,QAAQ,CAAC,EAAE;IAF/B,QAAQ,CAAC,IAAI,WAAW;IAExB,YAA6B,EAAE,EAAE,SAAS,EAAI;IAExC,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAOxF;IAEK,MAAM,CACV,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,gBAAgB,EAC5B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CA4B7C;CACF"}
@@ -0,0 +1,45 @@
1
+ import { query, toNumber } from '../sql.js';
2
+ import { statsFromRow } from './backend.js';
3
+ import { subjectAggregates, UNBOUNDED_MS, } from './metrics.js';
4
+ /**
5
+ * Reads the `lucid` sink's tables (`flags_exposures`, `flags_tracks`). Level 1 of DESIGN §12: fine
6
+ * up to tens of millions of events a month, especially with results computed by the worker and
7
+ * cached, as the provider does.
8
+ */
9
+ export class LucidBackend {
10
+ db;
11
+ kind = 'lucid';
12
+ constructor(db) {
13
+ this.db = db;
14
+ }
15
+ async exposures(experiment, environment) {
16
+ const rows = await query(this.db, 'SELECT variant, COUNT(*) AS n FROM flags_exposures WHERE environment = ? AND experiment = ? GROUP BY variant', [environment, experiment]);
17
+ return Object.fromEntries(rows.map((row) => [row.variant, toNumber(row.n)]));
18
+ }
19
+ async metric(experiment, definition, environment) {
20
+ const { post, pre } = subjectAggregates(definition);
21
+ const window = definition.windowMs ?? UNBOUNDED_MS;
22
+ const preWindow = definition.cupedMs ?? 0;
23
+ const rows = await query(this.db, `SELECT variant,
24
+ COUNT(*) AS n,
25
+ SUM(y) AS sum_y, SUM(y * y) AS sum_y2,
26
+ SUM(xv) AS sum_x, SUM(xv * xv) AS sum_x2, SUM(xv * y) AS sum_xy
27
+ FROM (
28
+ SELECT x.variant AS variant, x.subject AS subject,
29
+ ${post} AS y,
30
+ ${pre} AS xv
31
+ FROM flags_exposures x
32
+ LEFT JOIN flags_tracks t
33
+ ON t.subject = x.subject
34
+ AND t.environment = x.environment
35
+ AND t.name = ?
36
+ AND t.ts >= x.first_seen_ts - ?
37
+ AND t.ts < x.first_seen_ts + ?
38
+ WHERE x.environment = ? AND x.experiment = ?
39
+ GROUP BY x.variant, x.subject
40
+ ) per_subject
41
+ GROUP BY variant`, [definition.event, preWindow, window, environment, experiment]);
42
+ return Object.fromEntries(rows.map((row) => [String(row.variant), statsFromRow(row)]));
43
+ }
44
+ }
45
+ //# sourceMappingURL=lucid_backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lucid_backend.js","sourceRoot":"","sources":["../../../src/analytics/lucid_backend.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAkB,QAAQ,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EAAyB,YAAY,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAEL,iBAAiB,EACjB,YAAY,GAEb,MAAM,cAAc,CAAC;AAEtB;;;;GAIG;AACH,MAAM,OAAO,YAAY;IAGM,EAAE;IAFtB,IAAI,GAAG,OAAO,CAAC;IAExB,YAA6B,EAAa;kBAAb,EAAE;IAAc,CAAC;IAE9C,KAAK,CAAC,SAAS,CAAC,UAAkB,EAAE,WAAmB;QACrD,MAAM,IAAI,GAAG,MAAM,KAAK,CACtB,IAAI,CAAC,EAAE,EACP,8GAA8G,EAC9G,CAAC,WAAW,EAAE,UAAU,CAAC,CAC1B,CAAC;QACF,OAAO,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,CAAC;IAED,KAAK,CAAC,MAAM,CACV,UAAkB,EAClB,UAA4B,EAC5B,WAAmB;QAEnB,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;QACpD,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,IAAI,YAAY,CAAC;QACnD,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;QAC1C,MAAM,IAAI,GAAG,MAAM,KAAK,CACtB,IAAI,CAAC,EAAE,EACP;;;;;;oBAMc,IAAI;oBACJ,GAAG;;;;;;;;;;;yBAWE,EACnB,CAAC,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,CAAC,CAC/D,CAAC;QACF,OAAO,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACzF,CAAC;CACF"}
@@ -0,0 +1,50 @@
1
+ /** How a metric aggregates a subject's tracked events. */
2
+ export type MetricKind = 'conversion' | 'count' | 'sum';
3
+ export interface MetricOptions {
4
+ description?: string;
5
+ /** Count only events within this many ms after the subject's first exposure. Default: no limit. */
6
+ windowMs?: number;
7
+ /**
8
+ * CUPED: use the same metric over this many ms BEFORE exposure as a covariate. Shrinks variance
9
+ * (decisions with fewer subjects) when behaviour is stable over time. Default: off.
10
+ */
11
+ cupedMs?: number;
12
+ /** Which direction is good. Default `increase`. Shown in the console; guardrails alert on the bad one. */
13
+ direction?: 'increase' | 'decrease';
14
+ }
15
+ export interface MetricDefinition extends MetricOptions {
16
+ kind: MetricKind;
17
+ /** Tracked event name (`flags.track(subject, 'purchase')`). */
18
+ event: string;
19
+ }
20
+ /** Builders for `metrics` in `config/flags.ts`. */
21
+ export declare const metric: {
22
+ /** Share of subjects who did `event` at least once after exposure. */
23
+ conversion(event: string, options?: MetricOptions): MetricDefinition;
24
+ /** Times each subject did `event` after exposure, averaged per subject. */
25
+ count(event: string, options?: MetricOptions): MetricDefinition;
26
+ /** Sum of the events' `value` per subject (revenue per user), averaged per subject. */
27
+ sum(event: string, options?: MetricOptions): MetricDefinition;
28
+ };
29
+ /** Per-variant sufficient statistics of one metric (y) and its CUPED covariate (x). */
30
+ export interface VariantMetricStats {
31
+ n: number;
32
+ sumY: number;
33
+ sumY2: number;
34
+ sumX: number;
35
+ sumX2: number;
36
+ sumXY: number;
37
+ }
38
+ /**
39
+ * The per-subject aggregation, as SQL expressions over a joined track row `t` and exposure row `x`.
40
+ * Shared by every SQL backend so they agree on what a metric means. The time windows live in the
41
+ * expressions, not in a WHERE clause: a subject with no event in the window must still count, as a
42
+ * zero, in the denominator.
43
+ */
44
+ export declare function subjectAggregates(definition: MetricDefinition): {
45
+ post: string;
46
+ pre: string;
47
+ };
48
+ /** A window far beyond any real experiment, so "no limit" needs no special SQL. */
49
+ export declare const UNBOUNDED_MS: number;
50
+ //# sourceMappingURL=metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../../src/analytics/metrics.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,OAAO,GAAG,KAAK,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mGAAmG;IACnG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0GAA0G;IAC1G,SAAS,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;CACrC;AAED,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACrD,IAAI,EAAE,UAAU,CAAC;IACjB,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;CACf;AAED,mDAAmD;AACnD,eAAO,MAAM,MAAM;IACjB,sEAAsE;IACtE,UAAU,QAAQ,MAAM,YAAW,aAAa,GAAQ,gBAAgB;IAGxE,2EAA2E;IAC3E,KAAK,QAAQ,MAAM,YAAW,aAAa,GAAQ,gBAAgB;IAGnE,uFAAuF;IACvF,GAAG,QAAQ,MAAM,YAAW,aAAa,GAAQ,gBAAgB;CAGlE,CAAC;AAEF,uFAAuF;AACvF,MAAM,WAAW,kBAAkB;IACjC,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,gBAAgB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAc7F;AAED,mFAAmF;AACnF,eAAO,MAAM,YAAY,QAA+B,CAAC"}
@@ -0,0 +1,42 @@
1
+ /** Builders for `metrics` in `config/flags.ts`. */
2
+ export const metric = {
3
+ /** Share of subjects who did `event` at least once after exposure. */
4
+ conversion(event, options = {}) {
5
+ return { ...options, kind: 'conversion', event };
6
+ },
7
+ /** Times each subject did `event` after exposure, averaged per subject. */
8
+ count(event, options = {}) {
9
+ return { ...options, kind: 'count', event };
10
+ },
11
+ /** Sum of the events' `value` per subject (revenue per user), averaged per subject. */
12
+ sum(event, options = {}) {
13
+ return { ...options, kind: 'sum', event };
14
+ },
15
+ };
16
+ /**
17
+ * The per-subject aggregation, as SQL expressions over a joined track row `t` and exposure row `x`.
18
+ * Shared by every SQL backend so they agree on what a metric means. The time windows live in the
19
+ * expressions, not in a WHERE clause: a subject with no event in the window must still count, as a
20
+ * zero, in the denominator.
21
+ */
22
+ export function subjectAggregates(definition) {
23
+ const window = Math.trunc(Number(definition.windowMs ?? UNBOUNDED_MS));
24
+ const preWindow = Math.trunc(Number(definition.cupedMs ?? 0));
25
+ if (!Number.isFinite(window) || window <= 0)
26
+ throw new RangeError('metric windowMs must be > 0');
27
+ if (!Number.isFinite(preWindow) || preWindow < 0)
28
+ throw new RangeError('metric cupedMs must be ≥ 0');
29
+ const post = `t.ts >= x.first_seen_ts AND t.ts < x.first_seen_ts + ${window}`;
30
+ const pre = `t.ts < x.first_seen_ts AND t.ts >= x.first_seen_ts - ${preWindow}`;
31
+ const agg = (condition) => {
32
+ if (definition.kind === 'conversion')
33
+ return `MAX(CASE WHEN ${condition} THEN 1 ELSE 0 END)`;
34
+ if (definition.kind === 'count')
35
+ return `SUM(CASE WHEN ${condition} THEN 1 ELSE 0 END)`;
36
+ return `COALESCE(SUM(CASE WHEN ${condition} THEN t.value ELSE 0 END), 0)`;
37
+ };
38
+ return { post: agg(post), pre: preWindow > 0 ? agg(pre) : '0' };
39
+ }
40
+ /** A window far beyond any real experiment, so "no limit" needs no special SQL. */
41
+ export const UNBOUNDED_MS = 100 * 365 * 24 * 3600 * 1000;
42
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../../src/analytics/metrics.ts"],"names":[],"mappings":"AAsBA,mDAAmD;AACnD,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,sEAAsE;IACtE,UAAU,CAAC,KAAa,EAAE,OAAO,GAAkB,EAAE;QACnD,OAAO,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IACnD,CAAC;IACD,2EAA2E;IAC3E,KAAK,CAAC,KAAa,EAAE,OAAO,GAAkB,EAAE;QAC9C,OAAO,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9C,CAAC;IACD,uFAAuF;IACvF,GAAG,CAAC,KAAa,EAAE,OAAO,GAAkB,EAAE;QAC5C,OAAO,EAAE,GAAG,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAC5C,CAAC;CACF,CAAC;AAYF;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAA4B;IAC5D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,IAAI,YAAY,CAAC,CAAC,CAAC;IACvE,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,6BAA6B,CAAC,CAAC;IACjG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,GAAG,CAAC;QAC9C,MAAM,IAAI,UAAU,CAAC,4BAA4B,CAAC,CAAC;IACrD,MAAM,IAAI,GAAG,wDAAwD,MAAM,EAAE,CAAC;IAC9E,MAAM,GAAG,GAAG,wDAAwD,SAAS,EAAE,CAAC;IAChF,MAAM,GAAG,GAAG,CAAC,SAAiB,EAAE,EAAE;QAChC,IAAI,UAAU,CAAC,IAAI,KAAK,YAAY;YAAE,OAAO,iBAAiB,SAAS,qBAAqB,CAAC;QAC7F,IAAI,UAAU,CAAC,IAAI,KAAK,OAAO;YAAE,OAAO,iBAAiB,SAAS,qBAAqB,CAAC;QACxF,OAAO,0BAA0B,SAAS,+BAA+B,CAAC;IAC5E,CAAC,CAAC;IACF,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,SAAS,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;AAClE,CAAC;AAED,mFAAmF;AACnF,MAAM,CAAC,MAAM,YAAY,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC"}
@@ -0,0 +1,80 @@
1
+ import { type FlagDefinition, type SrmResult } from '@adonis-agora/flags-core';
2
+ import type { AnalyticsBackend } from './backend.js';
3
+ import type { MetricDefinition } from './metrics.js';
4
+ /** The variant name exposures of global-holdout subjects are recorded under. */
5
+ export declare const HOLDOUT_VARIANT = "__holdout__";
6
+ /** An experiment as the results service sees it. */
7
+ export interface ExperimentSpec {
8
+ /** Experiment key (the flag key unless `experiment.key` says otherwise). */
9
+ key: string;
10
+ flag: string;
11
+ /** Environment whose exposures and events are analysed. Default `production`. */
12
+ environment?: string;
13
+ /** Metric name → definition. */
14
+ metrics: Record<string, MetricDefinition>;
15
+ /** Metrics that must not get worse; a significant move the wrong way is flagged. */
16
+ guardrails?: string[];
17
+ /** Control variant. Default: the flag's `offVariant`. */
18
+ control?: string;
19
+ /** Confidence level for fixed-horizon intervals. Default 0.95. */
20
+ confidence?: number;
21
+ }
22
+ export interface Comparison {
23
+ variant: string;
24
+ test: 'proportion' | 'welch' | 'cuped';
25
+ difference: number;
26
+ relativeLift: number;
27
+ pValue: number;
28
+ interval: [number, number];
29
+ significant: boolean;
30
+ /** Always-valid (sequential) version of the same comparison: safe to peek. */
31
+ sequential: {
32
+ pValue: number;
33
+ interval: [number, number];
34
+ significant: boolean;
35
+ };
36
+ /** CUPED only. */
37
+ varianceReduction?: number;
38
+ /** `better` / `worse` according to the metric's direction, when significant. */
39
+ verdict: 'better' | 'worse' | 'inconclusive';
40
+ }
41
+ export interface MetricResult {
42
+ name: string;
43
+ kind: MetricDefinition['kind'];
44
+ event: string;
45
+ direction: 'increase' | 'decrease';
46
+ guardrail: boolean;
47
+ variants: Array<{
48
+ variant: string;
49
+ n: number;
50
+ mean: number;
51
+ }>;
52
+ comparisons: Comparison[];
53
+ /** A guardrail that got significantly worse in some variant. */
54
+ alert: boolean;
55
+ }
56
+ export interface ExperimentResults {
57
+ experiment: string;
58
+ flag: string;
59
+ environment: string;
60
+ control: string;
61
+ computedAt: string;
62
+ exposures: Record<string, number>;
63
+ /** Subjects of the global holdout who reached this experiment (not compared). */
64
+ holdout: number;
65
+ /** `null` when the flag has no single split to compare against. */
66
+ srm: (SrmResult & {
67
+ expected: Record<string, number>;
68
+ }) | null;
69
+ metrics: MetricResult[];
70
+ /** Comparisons computed with too few subjects (< 2 per variant) are skipped and listed here. */
71
+ warnings: string[];
72
+ }
73
+ /**
74
+ * The configured split weights, when the flag assigns through exactly one split (fallthrough or a
75
+ * single rule). SRM is only meaningful against one known allocation.
76
+ */
77
+ export declare function expectedWeights(flag: FlagDefinition): Record<string, number> | null;
78
+ /** Computes everything the console shows for an experiment. */
79
+ export declare function computeResults(backend: AnalyticsBackend, spec: ExperimentSpec, flag: FlagDefinition | undefined, previous?: ExperimentResults): Promise<ExperimentResults>;
80
+ //# sourceMappingURL=results.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../../src/analytics/results.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,143 @@
1
+ import { cupedTest, proportionTest, sequentialTest, srmTest, welchTest, } from '@adonis-agora/flags-core';
2
+ /** The variant name exposures of global-holdout subjects are recorded under. */
3
+ export const HOLDOUT_VARIANT = '__holdout__';
4
+ /**
5
+ * The configured split weights, when the flag assigns through exactly one split (fallthrough or a
6
+ * single rule). SRM is only meaningful against one known allocation.
7
+ */
8
+ export function expectedWeights(flag) {
9
+ const splits = [flag.fallthrough, ...(flag.rules ?? []).map((rule) => rule.serve)].filter((serve) => 'split' in serve);
10
+ if (splits.length !== 1)
11
+ return null;
12
+ const weights = {};
13
+ for (const arm of splits[0].split)
14
+ if (arm.weight > 0)
15
+ weights[arm.variant] = (weights[arm.variant] ?? 0) + arm.weight;
16
+ return weights;
17
+ }
18
+ function asCuped(stats) {
19
+ return stats;
20
+ }
21
+ /** Computes everything the console shows for an experiment. */
22
+ export async function computeResults(backend, spec, flag, previous) {
23
+ const control = spec.control ?? flag?.offVariant ?? 'control';
24
+ const confidence = spec.confidence ?? 0.95;
25
+ const warnings = [];
26
+ const environment = spec.environment ?? 'production';
27
+ const allExposures = await backend.exposures(spec.key, environment);
28
+ // Global-holdout subjects are recorded under a reserved variant; they are not an arm of this
29
+ // experiment, so they are reported apart and left out of every comparison.
30
+ const holdout = allExposures[HOLDOUT_VARIANT] ?? 0;
31
+ const exposures = Object.fromEntries(Object.entries(allExposures).filter(([variant]) => variant !== HOLDOUT_VARIANT));
32
+ let srm = null;
33
+ const expected = flag ? expectedWeights(flag) : null;
34
+ if (expected && Object.keys(expected).length >= 2) {
35
+ const variants = Object.keys(expected);
36
+ const observed = variants.map((variant) => exposures[variant] ?? 0);
37
+ if (observed.reduce((a, b) => a + b, 0) > 0) {
38
+ srm = {
39
+ ...srmTest(observed, variants.map((v) => expected[v])),
40
+ expected,
41
+ };
42
+ }
43
+ }
44
+ const metrics = [];
45
+ for (const [name, definition] of Object.entries(spec.metrics)) {
46
+ const { [HOLDOUT_VARIANT]: _held, ...stats } = await backend.metric(spec.key, definition, environment);
47
+ const direction = definition.direction ?? 'increase';
48
+ const guardrail = spec.guardrails?.includes(name) ?? false;
49
+ const variantNames = [...new Set([...Object.keys(exposures), ...Object.keys(stats)])].sort((a, b) => (a === control ? -1 : b === control ? 1 : a.localeCompare(b)));
50
+ const result = {
51
+ name,
52
+ kind: definition.kind,
53
+ event: definition.event,
54
+ direction,
55
+ guardrail,
56
+ variants: variantNames.map((variant) => {
57
+ const s = stats[variant];
58
+ return { variant, n: s?.n ?? 0, mean: s && s.n > 0 ? s.sumY / s.n : 0 };
59
+ }),
60
+ comparisons: [],
61
+ alert: false,
62
+ };
63
+ const base = stats[control];
64
+ for (const variant of variantNames) {
65
+ if (variant === control)
66
+ continue;
67
+ const treatment = stats[variant];
68
+ if (!base || !treatment || base.n < 2 || treatment.n < 2) {
69
+ warnings.push(`${name}: not enough subjects to compare "${variant}" with "${control}" yet`);
70
+ continue;
71
+ }
72
+ const prior = previous?.metrics
73
+ .find((m) => m.name === name)
74
+ ?.comparisons.find((c) => c.variant === variant)?.sequential.pValue;
75
+ const plainControl = { n: base.n, sum: base.sumY, sumSquares: base.sumY2 };
76
+ const plainTreatment = { n: treatment.n, sum: treatment.sumY, sumSquares: treatment.sumY2 };
77
+ let comparison;
78
+ if (definition.cupedMs && definition.cupedMs > 0) {
79
+ const r = cupedTest(asCuped(base), asCuped(treatment), confidence);
80
+ comparison = {
81
+ test: 'cuped',
82
+ difference: r.difference,
83
+ relativeLift: r.relativeLift,
84
+ pValue: r.pValue,
85
+ interval: r.interval,
86
+ significant: r.pValue < 1 - confidence,
87
+ varianceReduction: r.varianceReduction,
88
+ };
89
+ }
90
+ else if (definition.kind === 'conversion') {
91
+ const r = proportionTest({ n: base.n, conversions: base.sumY }, { n: treatment.n, conversions: treatment.sumY }, confidence);
92
+ comparison = {
93
+ test: 'proportion',
94
+ difference: r.difference,
95
+ relativeLift: r.relativeLift,
96
+ pValue: r.pValue,
97
+ interval: r.interval,
98
+ significant: r.pValue < 1 - confidence,
99
+ };
100
+ }
101
+ else {
102
+ const r = welchTest(plainControl, plainTreatment, confidence);
103
+ comparison = {
104
+ test: 'welch',
105
+ difference: r.difference,
106
+ relativeLift: r.relativeLift,
107
+ pValue: r.pValue,
108
+ interval: r.interval,
109
+ significant: r.pValue < 1 - confidence,
110
+ };
111
+ }
112
+ const seq = sequentialTest(plainControl, plainTreatment, {
113
+ alpha: 1 - confidence,
114
+ ...(prior !== undefined ? { previousPValue: prior } : {}),
115
+ });
116
+ const improves = direction === 'increase' ? comparison.difference > 0 : comparison.difference < 0;
117
+ const verdict = comparison.significant ? (improves ? 'better' : 'worse') : 'inconclusive';
118
+ result.comparisons.push({
119
+ variant,
120
+ ...comparison,
121
+ sequential: { pValue: seq.pValue, interval: seq.interval, significant: seq.significant },
122
+ verdict,
123
+ });
124
+ // Guardrails alert on the always-valid test, so watching them daily does not cry wolf.
125
+ if (guardrail && seq.significant && !improves)
126
+ result.alert = true;
127
+ }
128
+ metrics.push(result);
129
+ }
130
+ return {
131
+ experiment: spec.key,
132
+ flag: spec.flag,
133
+ environment,
134
+ control,
135
+ computedAt: new Date().toISOString(),
136
+ exposures,
137
+ holdout,
138
+ srm,
139
+ metrics,
140
+ warnings,
141
+ };
142
+ }
143
+ //# sourceMappingURL=results.js.map