@findlane/server 0.31.0 → 0.32.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.
package/README.md CHANGED
@@ -60,7 +60,8 @@ const results = await products.search({ q }, { visitor: { ip: req.ip, userAgent:
60
60
  | `getSearchTests()`, `setSearchTests(tests)` | Searches that must keep working, saved apart from the settings, such as a record in the top 3 or at least 100 results. `compareSettings` checks them. |
61
61
  | `getIndexingStatus()`, `waitUntilIndexed(revision)` | Writes return an accepted revision and are searchable when accepted, so `waitUntilIndexed` returns at once. |
62
62
  | `getPublicSearchAccess()`, `setPublicSearchPolicy(policy)`, `usePublicIndexDefaults()` | Restrict what browser search may read and query. |
63
- | `events.send(events, { visitorId, visitId }?)`, `events.importOrders(orders)`, `events.cancelOrder(orderID)` | Orders and other events for the index's Events page and Bought together; the key needs the `events` or `write` scope. Your own anonymous visitor IDs link a shopper's events, when the index takes them. |
63
+ | `events.send(events, { visitorId, visitId, abTest }?)`, `events.importOrders(orders)`, `events.cancelOrder(orderID)` | Orders and other events for the index's Events page and Bought together; the key needs the `events` or `write` scope. Your own anonymous visitor IDs link a shopper's events, when the index takes them. `abTest`, from the shopper's search answer, puts them in their A/B test group. |
64
+ | `abTests.get()`, `abTests.estimate({ share })`, `analytics.abTest()` | The index's A/B tests of its settings, with their results, and what a test would take. Owners start and end them in the dashboard. |
64
65
  | `events.forget(ids)` | Deletes the events sent with these visitor or visit IDs, within a week. |
65
66
  | `events.proxy(request)`, `events.relay(body, visitor)` | A route on your own site for the browser SDK's events (its `eventsUrl`), which ad blockers let through. See [Events](https://findlane.dev/docs/events#through-your-own-domain). |
66
67
  | `analytics.searches()`, `.queries({ list, limit })`, `.filters()`, `.events()`, `.products()`, `.productCounts()`, `.product(objectID)` | The index's analytics, for your own admin: searches over time, top queries with their clicks, queries without results or clicks, the filters used, events, and each product's views, add-to-carts and orders. The key needs the `analytics` scope, and works only from a server. See [Analytics](https://findlane.dev/docs/api#analytics). |
package/dist/index.cjs CHANGED
@@ -241,7 +241,8 @@ class SearchServerClient {
241
241
  return jsonResponse(await response.text(), response.status, retryAfter ? { "retry-after": retryAfter } : {});
242
242
  };
243
243
  const sendEvents = async (events, options = {}) => {
244
- const { signal, visitorId, visitId } = options;
244
+ const { signal, visitorId, visitId, abTest } = options;
245
+ const label = abTest === undefined || typeof abTest === "string" ? abTest : `${abTest.id}:${abTest.variant}`;
245
246
  const result = { accepted: 0, rejected: [] };
246
247
  let batch = [];
247
248
  let sent = 0;
@@ -251,6 +252,7 @@ class SearchServerClient {
251
252
  const answer = await postEvents({
252
253
  ...visitorId === undefined ? {} : { visitorId },
253
254
  ...visitId === undefined ? {} : { visitId },
255
+ ...label === undefined ? {} : { abTest: label },
254
256
  events: batch
255
257
  }, signal);
256
258
  result.accepted += answer.accepted ?? 0;
@@ -394,7 +396,12 @@ class SearchServerClient {
394
396
  product: (objectID, options = {}) => report(`products/${encodeURIComponent(objectID)}`, options),
395
397
  revenue: (options = {}) => report("revenue", options),
396
398
  filterSales: (options = {}) => report("filter-sales", options),
397
- banners: (options = {}) => request(`/banners?range=${options.range ?? "30d"}`, "GET", undefined, options.signal)
399
+ banners: (options = {}) => request(`/banners?range=${options.range ?? "30d"}`, "GET", undefined, options.signal),
400
+ abTest: (options = {}) => request("/analytics/ab-test", "GET", undefined, options.signal)
401
+ },
402
+ abTests: {
403
+ get: (signal) => request("/ab-tests", "GET", undefined, signal),
404
+ estimate: (options = {}) => request(`/ab-tests/estimate${options.share === undefined ? "" : `?share=${options.share}`}`, "GET", undefined, options.signal)
398
405
  },
399
406
  upsert: (records, signal) => request("/records", "POST", { records }, signal),
400
407
  getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
package/dist/index.d.cts CHANGED
@@ -974,6 +974,80 @@ export type EventOptions = {
974
974
  signal?: AbortSignal;
975
975
  visitorId?: string;
976
976
  visitId?: string;
977
+ /** The shopper's group in the index's running A/B test (0.32.0): the `abTest` of the search answer their browser got,
978
+ * as `{ id, variant }` or `"<id>:<variant>"`, which the browser SDK sends with its own events. Your server's events
979
+ * count toward a group only with it, so send one shopper's events a call. A label for a test that ended is ignored. */
980
+ abTest?: AbTestLabel | string;
981
+ };
982
+ /** A shopper's group in an A/B test, as a browser search answer's `abTest` gives it: `a` searched with today's
983
+ * settings, `b` with the change (0.32.0). */
984
+ export type AbTestLabel = {
985
+ id: string;
986
+ variant: "a" | "b";
987
+ };
988
+ /** One measure in an A/B test's results: each group's value per search (a share for `noResults` and `topThree`), and
989
+ * B's change over A (0.1 is 10% more) with its 95% interval. Null where there aren't the searches or days yet. */
990
+ export type AbFigure = {
991
+ a: number | null;
992
+ b: number | null;
993
+ change: number | null;
994
+ low: number | null;
995
+ high: number | null;
996
+ };
997
+ /** An A/B test's results. `decided` once the test has run its days with 1,000 searches in each group; `verdict` is then
998
+ * `b` (the change did better), `a` (worse) or `none` (no clear difference), judged on clicks per search, then carts. */
999
+ export type AbTestResults = {
1000
+ days: number;
1001
+ decided: boolean;
1002
+ verdict: "a" | "b" | "none" | null;
1003
+ searches: {
1004
+ a: number;
1005
+ b: number;
1006
+ };
1007
+ currency: string | null;
1008
+ figures: Record<"noResults" | "clicks" | "topThree" | "carts" | "orders" | "revenue", AbFigure>;
1009
+ perDay: {
1010
+ day: string;
1011
+ searches: {
1012
+ a: number;
1013
+ b: number;
1014
+ };
1015
+ clicks: {
1016
+ a: number;
1017
+ b: number;
1018
+ };
1019
+ }[];
1020
+ computedAt: string;
1021
+ };
1022
+ /** An A/B test of an index's settings (0.32.0): group B's `share` of shoppers, the settings it `changes` and their values
1023
+ * in `patch` (null where B goes without one), an A/A test (`aa`) changes none. `outcome` once ended: `a` kept today's
1024
+ * settings, `b` applied the change, `guard` ended early because the change found nothing far more often. */
1025
+ export type AbTest = {
1026
+ id: string;
1027
+ name: string;
1028
+ share: number;
1029
+ changes: string[];
1030
+ aa: boolean;
1031
+ patch: Record<string, unknown>;
1032
+ startedAt: string;
1033
+ endsAt: string;
1034
+ endedAt: string | null;
1035
+ outcome: "a" | "b" | "guard" | null;
1036
+ results: AbTestResults | null;
1037
+ };
1038
+ /** An index's A/B tests: the running one with its results so far, or null, and up to 10 that ended, newest first. */
1039
+ export type AbTests = {
1040
+ running: AbTest | null;
1041
+ ended: AbTest[];
1042
+ };
1043
+ /** What a test at a share would take: the days to find a 10% change in clicks per search (null with too few
1044
+ * searches), from the index's searches and clicks per search over the last 14 days, and the share of searches more
1045
+ * that would miss the cache. */
1046
+ export type AbTestEstimate = {
1047
+ searchesPerDay: number;
1048
+ clicksPerSearch: number;
1049
+ days: number | null;
1050
+ extraMisses: number;
977
1051
  };
978
1052
  /** An analytics window, in UTC: `range` (7d by default), or `from` and `to`. A date as `from` is the start of
979
1053
  * that day, and as `to` includes the whole day; `to` is now by default. Analytics go back 90 days: a window
@@ -1464,6 +1538,29 @@ export declare class SearchServerClient {
1464
1538
  range?: "7d" | "30d" | "90d";
1465
1539
  signal?: AbortSignal;
1466
1540
  }) => Promise<BannerStatsReport>;
1541
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
1542
+ abTest: (options?: {
1543
+ signal?: AbortSignal;
1544
+ }) => Promise<AbTests & {
1545
+ index: string;
1546
+ }>;
1547
+ };
1548
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
1549
+ abTests: {
1550
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
1551
+ get: (signal?: AbortSignal) => Promise<AbTests & {
1552
+ shares: number[];
1553
+ days: {
1554
+ min: number;
1555
+ default: number;
1556
+ max: number;
1557
+ };
1558
+ }>;
1559
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
1560
+ estimate: (options?: {
1561
+ share?: number;
1562
+ signal?: AbortSignal;
1563
+ }) => Promise<AbTestEstimate>;
1467
1564
  };
1468
1565
  upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
1469
1566
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
@@ -1672,6 +1769,29 @@ export declare class SearchServerClient {
1672
1769
  range?: "7d" | "30d" | "90d";
1673
1770
  signal?: AbortSignal;
1674
1771
  }) => Promise<BannerStatsReport>;
1772
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
1773
+ abTest: (options?: {
1774
+ signal?: AbortSignal;
1775
+ }) => Promise<AbTests & {
1776
+ index: string;
1777
+ }>;
1778
+ };
1779
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
1780
+ abTests: {
1781
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
1782
+ get: (signal?: AbortSignal) => Promise<AbTests & {
1783
+ shares: number[];
1784
+ days: {
1785
+ min: number;
1786
+ default: number;
1787
+ max: number;
1788
+ };
1789
+ }>;
1790
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
1791
+ estimate: (options?: {
1792
+ share?: number;
1793
+ signal?: AbortSignal;
1794
+ }) => Promise<AbTestEstimate>;
1675
1795
  };
1676
1796
  upsert: (records: S[], signal?: AbortSignal) => Promise<WriteReceipt>;
1677
1797
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S>>;
@@ -1878,6 +1998,29 @@ export declare class SearchServerClient {
1878
1998
  range?: "7d" | "30d" | "90d";
1879
1999
  signal?: AbortSignal;
1880
2000
  }) => Promise<BannerStatsReport>;
2001
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2002
+ abTest: (options?: {
2003
+ signal?: AbortSignal;
2004
+ }) => Promise<AbTests & {
2005
+ index: string;
2006
+ }>;
2007
+ };
2008
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2009
+ abTests: {
2010
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2011
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2012
+ shares: number[];
2013
+ days: {
2014
+ min: number;
2015
+ default: number;
2016
+ max: number;
2017
+ };
2018
+ }>;
2019
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2020
+ estimate: (options?: {
2021
+ share?: number;
2022
+ signal?: AbortSignal;
2023
+ }) => Promise<AbTestEstimate>;
1881
2024
  };
1882
2025
  upsert: (records: S_1[], signal?: AbortSignal) => Promise<WriteReceipt>;
1883
2026
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_1>>;
@@ -2084,6 +2227,29 @@ export declare class SearchServerClient {
2084
2227
  range?: "7d" | "30d" | "90d";
2085
2228
  signal?: AbortSignal;
2086
2229
  }) => Promise<BannerStatsReport>;
2230
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2231
+ abTest: (options?: {
2232
+ signal?: AbortSignal;
2233
+ }) => Promise<AbTests & {
2234
+ index: string;
2235
+ }>;
2236
+ };
2237
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2238
+ abTests: {
2239
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2240
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2241
+ shares: number[];
2242
+ days: {
2243
+ min: number;
2244
+ default: number;
2245
+ max: number;
2246
+ };
2247
+ }>;
2248
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2249
+ estimate: (options?: {
2250
+ share?: number;
2251
+ signal?: AbortSignal;
2252
+ }) => Promise<AbTestEstimate>;
2087
2253
  };
2088
2254
  upsert: (records: S_2[], signal?: AbortSignal) => Promise<WriteReceipt>;
2089
2255
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_2>>;
@@ -2290,6 +2456,29 @@ export declare class SearchServerClient {
2290
2456
  range?: "7d" | "30d" | "90d";
2291
2457
  signal?: AbortSignal;
2292
2458
  }) => Promise<BannerStatsReport>;
2459
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2460
+ abTest: (options?: {
2461
+ signal?: AbortSignal;
2462
+ }) => Promise<AbTests & {
2463
+ index: string;
2464
+ }>;
2465
+ };
2466
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2467
+ abTests: {
2468
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2469
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2470
+ shares: number[];
2471
+ days: {
2472
+ min: number;
2473
+ default: number;
2474
+ max: number;
2475
+ };
2476
+ }>;
2477
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2478
+ estimate: (options?: {
2479
+ share?: number;
2480
+ signal?: AbortSignal;
2481
+ }) => Promise<AbTestEstimate>;
2293
2482
  };
2294
2483
  upsert: (records: S_3[], signal?: AbortSignal) => Promise<WriteReceipt>;
2295
2484
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_3>>;
@@ -2496,6 +2685,29 @@ export declare class SearchServerClient {
2496
2685
  range?: "7d" | "30d" | "90d";
2497
2686
  signal?: AbortSignal;
2498
2687
  }) => Promise<BannerStatsReport>;
2688
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2689
+ abTest: (options?: {
2690
+ signal?: AbortSignal;
2691
+ }) => Promise<AbTests & {
2692
+ index: string;
2693
+ }>;
2694
+ };
2695
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2696
+ abTests: {
2697
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2698
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2699
+ shares: number[];
2700
+ days: {
2701
+ min: number;
2702
+ default: number;
2703
+ max: number;
2704
+ };
2705
+ }>;
2706
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2707
+ estimate: (options?: {
2708
+ share?: number;
2709
+ signal?: AbortSignal;
2710
+ }) => Promise<AbTestEstimate>;
2499
2711
  };
2500
2712
  upsert: (records: S_4[], signal?: AbortSignal) => Promise<WriteReceipt>;
2501
2713
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_4>>;
@@ -2702,6 +2914,29 @@ export declare class SearchServerClient {
2702
2914
  range?: "7d" | "30d" | "90d";
2703
2915
  signal?: AbortSignal;
2704
2916
  }) => Promise<BannerStatsReport>;
2917
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2918
+ abTest: (options?: {
2919
+ signal?: AbortSignal;
2920
+ }) => Promise<AbTests & {
2921
+ index: string;
2922
+ }>;
2923
+ };
2924
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2925
+ abTests: {
2926
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2927
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2928
+ shares: number[];
2929
+ days: {
2930
+ min: number;
2931
+ default: number;
2932
+ max: number;
2933
+ };
2934
+ }>;
2935
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2936
+ estimate: (options?: {
2937
+ share?: number;
2938
+ signal?: AbortSignal;
2939
+ }) => Promise<AbTestEstimate>;
2705
2940
  };
2706
2941
  upsert: (records: S_5[], signal?: AbortSignal) => Promise<WriteReceipt>;
2707
2942
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_5>>;
@@ -2908,6 +3143,29 @@ export declare class SearchServerClient {
2908
3143
  range?: "7d" | "30d" | "90d";
2909
3144
  signal?: AbortSignal;
2910
3145
  }) => Promise<BannerStatsReport>;
3146
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3147
+ abTest: (options?: {
3148
+ signal?: AbortSignal;
3149
+ }) => Promise<AbTests & {
3150
+ index: string;
3151
+ }>;
3152
+ };
3153
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3154
+ abTests: {
3155
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3156
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3157
+ shares: number[];
3158
+ days: {
3159
+ min: number;
3160
+ default: number;
3161
+ max: number;
3162
+ };
3163
+ }>;
3164
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3165
+ estimate: (options?: {
3166
+ share?: number;
3167
+ signal?: AbortSignal;
3168
+ }) => Promise<AbTestEstimate>;
2911
3169
  };
2912
3170
  upsert: (records: S_6[], signal?: AbortSignal) => Promise<WriteReceipt>;
2913
3171
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_6>>;
@@ -3114,6 +3372,29 @@ export declare class SearchServerClient {
3114
3372
  range?: "7d" | "30d" | "90d";
3115
3373
  signal?: AbortSignal;
3116
3374
  }) => Promise<BannerStatsReport>;
3375
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3376
+ abTest: (options?: {
3377
+ signal?: AbortSignal;
3378
+ }) => Promise<AbTests & {
3379
+ index: string;
3380
+ }>;
3381
+ };
3382
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3383
+ abTests: {
3384
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3385
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3386
+ shares: number[];
3387
+ days: {
3388
+ min: number;
3389
+ default: number;
3390
+ max: number;
3391
+ };
3392
+ }>;
3393
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3394
+ estimate: (options?: {
3395
+ share?: number;
3396
+ signal?: AbortSignal;
3397
+ }) => Promise<AbTestEstimate>;
3117
3398
  };
3118
3399
  upsert: (records: S_7[], signal?: AbortSignal) => Promise<WriteReceipt>;
3119
3400
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_7>>;
@@ -3320,6 +3601,29 @@ export declare class SearchServerClient {
3320
3601
  range?: "7d" | "30d" | "90d";
3321
3602
  signal?: AbortSignal;
3322
3603
  }) => Promise<BannerStatsReport>;
3604
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3605
+ abTest: (options?: {
3606
+ signal?: AbortSignal;
3607
+ }) => Promise<AbTests & {
3608
+ index: string;
3609
+ }>;
3610
+ };
3611
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3612
+ abTests: {
3613
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3614
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3615
+ shares: number[];
3616
+ days: {
3617
+ min: number;
3618
+ default: number;
3619
+ max: number;
3620
+ };
3621
+ }>;
3622
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3623
+ estimate: (options?: {
3624
+ share?: number;
3625
+ signal?: AbortSignal;
3626
+ }) => Promise<AbTestEstimate>;
3323
3627
  };
3324
3628
  upsert: (records: S_8[], signal?: AbortSignal) => Promise<WriteReceipt>;
3325
3629
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_8>>;
@@ -3526,6 +3830,29 @@ export declare class SearchServerClient {
3526
3830
  range?: "7d" | "30d" | "90d";
3527
3831
  signal?: AbortSignal;
3528
3832
  }) => Promise<BannerStatsReport>;
3833
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3834
+ abTest: (options?: {
3835
+ signal?: AbortSignal;
3836
+ }) => Promise<AbTests & {
3837
+ index: string;
3838
+ }>;
3839
+ };
3840
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3841
+ abTests: {
3842
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3843
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3844
+ shares: number[];
3845
+ days: {
3846
+ min: number;
3847
+ default: number;
3848
+ max: number;
3849
+ };
3850
+ }>;
3851
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3852
+ estimate: (options?: {
3853
+ share?: number;
3854
+ signal?: AbortSignal;
3855
+ }) => Promise<AbTestEstimate>;
3529
3856
  };
3530
3857
  upsert: (records: S_9[], signal?: AbortSignal) => Promise<WriteReceipt>;
3531
3858
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_9>>;
package/dist/index.d.ts CHANGED
@@ -974,6 +974,80 @@ export type EventOptions = {
974
974
  signal?: AbortSignal;
975
975
  visitorId?: string;
976
976
  visitId?: string;
977
+ /** The shopper's group in the index's running A/B test (0.32.0): the `abTest` of the search answer their browser got,
978
+ * as `{ id, variant }` or `"<id>:<variant>"`, which the browser SDK sends with its own events. Your server's events
979
+ * count toward a group only with it, so send one shopper's events a call. A label for a test that ended is ignored. */
980
+ abTest?: AbTestLabel | string;
981
+ };
982
+ /** A shopper's group in an A/B test, as a browser search answer's `abTest` gives it: `a` searched with today's
983
+ * settings, `b` with the change (0.32.0). */
984
+ export type AbTestLabel = {
985
+ id: string;
986
+ variant: "a" | "b";
987
+ };
988
+ /** One measure in an A/B test's results: each group's value per search (a share for `noResults` and `topThree`), and
989
+ * B's change over A (0.1 is 10% more) with its 95% interval. Null where there aren't the searches or days yet. */
990
+ export type AbFigure = {
991
+ a: number | null;
992
+ b: number | null;
993
+ change: number | null;
994
+ low: number | null;
995
+ high: number | null;
996
+ };
997
+ /** An A/B test's results. `decided` once the test has run its days with 1,000 searches in each group; `verdict` is then
998
+ * `b` (the change did better), `a` (worse) or `none` (no clear difference), judged on clicks per search, then carts. */
999
+ export type AbTestResults = {
1000
+ days: number;
1001
+ decided: boolean;
1002
+ verdict: "a" | "b" | "none" | null;
1003
+ searches: {
1004
+ a: number;
1005
+ b: number;
1006
+ };
1007
+ currency: string | null;
1008
+ figures: Record<"noResults" | "clicks" | "topThree" | "carts" | "orders" | "revenue", AbFigure>;
1009
+ perDay: {
1010
+ day: string;
1011
+ searches: {
1012
+ a: number;
1013
+ b: number;
1014
+ };
1015
+ clicks: {
1016
+ a: number;
1017
+ b: number;
1018
+ };
1019
+ }[];
1020
+ computedAt: string;
1021
+ };
1022
+ /** An A/B test of an index's settings (0.32.0): group B's `share` of shoppers, the settings it `changes` and their values
1023
+ * in `patch` (null where B goes without one), an A/A test (`aa`) changes none. `outcome` once ended: `a` kept today's
1024
+ * settings, `b` applied the change, `guard` ended early because the change found nothing far more often. */
1025
+ export type AbTest = {
1026
+ id: string;
1027
+ name: string;
1028
+ share: number;
1029
+ changes: string[];
1030
+ aa: boolean;
1031
+ patch: Record<string, unknown>;
1032
+ startedAt: string;
1033
+ endsAt: string;
1034
+ endedAt: string | null;
1035
+ outcome: "a" | "b" | "guard" | null;
1036
+ results: AbTestResults | null;
1037
+ };
1038
+ /** An index's A/B tests: the running one with its results so far, or null, and up to 10 that ended, newest first. */
1039
+ export type AbTests = {
1040
+ running: AbTest | null;
1041
+ ended: AbTest[];
1042
+ };
1043
+ /** What a test at a share would take: the days to find a 10% change in clicks per search (null with too few
1044
+ * searches), from the index's searches and clicks per search over the last 14 days, and the share of searches more
1045
+ * that would miss the cache. */
1046
+ export type AbTestEstimate = {
1047
+ searchesPerDay: number;
1048
+ clicksPerSearch: number;
1049
+ days: number | null;
1050
+ extraMisses: number;
977
1051
  };
978
1052
  /** An analytics window, in UTC: `range` (7d by default), or `from` and `to`. A date as `from` is the start of
979
1053
  * that day, and as `to` includes the whole day; `to` is now by default. Analytics go back 90 days: a window
@@ -1464,6 +1538,29 @@ export declare class SearchServerClient {
1464
1538
  range?: "7d" | "30d" | "90d";
1465
1539
  signal?: AbortSignal;
1466
1540
  }) => Promise<BannerStatsReport>;
1541
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
1542
+ abTest: (options?: {
1543
+ signal?: AbortSignal;
1544
+ }) => Promise<AbTests & {
1545
+ index: string;
1546
+ }>;
1547
+ };
1548
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
1549
+ abTests: {
1550
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
1551
+ get: (signal?: AbortSignal) => Promise<AbTests & {
1552
+ shares: number[];
1553
+ days: {
1554
+ min: number;
1555
+ default: number;
1556
+ max: number;
1557
+ };
1558
+ }>;
1559
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
1560
+ estimate: (options?: {
1561
+ share?: number;
1562
+ signal?: AbortSignal;
1563
+ }) => Promise<AbTestEstimate>;
1467
1564
  };
1468
1565
  upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
1469
1566
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
@@ -1672,6 +1769,29 @@ export declare class SearchServerClient {
1672
1769
  range?: "7d" | "30d" | "90d";
1673
1770
  signal?: AbortSignal;
1674
1771
  }) => Promise<BannerStatsReport>;
1772
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
1773
+ abTest: (options?: {
1774
+ signal?: AbortSignal;
1775
+ }) => Promise<AbTests & {
1776
+ index: string;
1777
+ }>;
1778
+ };
1779
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
1780
+ abTests: {
1781
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
1782
+ get: (signal?: AbortSignal) => Promise<AbTests & {
1783
+ shares: number[];
1784
+ days: {
1785
+ min: number;
1786
+ default: number;
1787
+ max: number;
1788
+ };
1789
+ }>;
1790
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
1791
+ estimate: (options?: {
1792
+ share?: number;
1793
+ signal?: AbortSignal;
1794
+ }) => Promise<AbTestEstimate>;
1675
1795
  };
1676
1796
  upsert: (records: S[], signal?: AbortSignal) => Promise<WriteReceipt>;
1677
1797
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S>>;
@@ -1878,6 +1998,29 @@ export declare class SearchServerClient {
1878
1998
  range?: "7d" | "30d" | "90d";
1879
1999
  signal?: AbortSignal;
1880
2000
  }) => Promise<BannerStatsReport>;
2001
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2002
+ abTest: (options?: {
2003
+ signal?: AbortSignal;
2004
+ }) => Promise<AbTests & {
2005
+ index: string;
2006
+ }>;
2007
+ };
2008
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2009
+ abTests: {
2010
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2011
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2012
+ shares: number[];
2013
+ days: {
2014
+ min: number;
2015
+ default: number;
2016
+ max: number;
2017
+ };
2018
+ }>;
2019
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2020
+ estimate: (options?: {
2021
+ share?: number;
2022
+ signal?: AbortSignal;
2023
+ }) => Promise<AbTestEstimate>;
1881
2024
  };
1882
2025
  upsert: (records: S_1[], signal?: AbortSignal) => Promise<WriteReceipt>;
1883
2026
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_1>>;
@@ -2084,6 +2227,29 @@ export declare class SearchServerClient {
2084
2227
  range?: "7d" | "30d" | "90d";
2085
2228
  signal?: AbortSignal;
2086
2229
  }) => Promise<BannerStatsReport>;
2230
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2231
+ abTest: (options?: {
2232
+ signal?: AbortSignal;
2233
+ }) => Promise<AbTests & {
2234
+ index: string;
2235
+ }>;
2236
+ };
2237
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2238
+ abTests: {
2239
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2240
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2241
+ shares: number[];
2242
+ days: {
2243
+ min: number;
2244
+ default: number;
2245
+ max: number;
2246
+ };
2247
+ }>;
2248
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2249
+ estimate: (options?: {
2250
+ share?: number;
2251
+ signal?: AbortSignal;
2252
+ }) => Promise<AbTestEstimate>;
2087
2253
  };
2088
2254
  upsert: (records: S_2[], signal?: AbortSignal) => Promise<WriteReceipt>;
2089
2255
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_2>>;
@@ -2290,6 +2456,29 @@ export declare class SearchServerClient {
2290
2456
  range?: "7d" | "30d" | "90d";
2291
2457
  signal?: AbortSignal;
2292
2458
  }) => Promise<BannerStatsReport>;
2459
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2460
+ abTest: (options?: {
2461
+ signal?: AbortSignal;
2462
+ }) => Promise<AbTests & {
2463
+ index: string;
2464
+ }>;
2465
+ };
2466
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2467
+ abTests: {
2468
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2469
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2470
+ shares: number[];
2471
+ days: {
2472
+ min: number;
2473
+ default: number;
2474
+ max: number;
2475
+ };
2476
+ }>;
2477
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2478
+ estimate: (options?: {
2479
+ share?: number;
2480
+ signal?: AbortSignal;
2481
+ }) => Promise<AbTestEstimate>;
2293
2482
  };
2294
2483
  upsert: (records: S_3[], signal?: AbortSignal) => Promise<WriteReceipt>;
2295
2484
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_3>>;
@@ -2496,6 +2685,29 @@ export declare class SearchServerClient {
2496
2685
  range?: "7d" | "30d" | "90d";
2497
2686
  signal?: AbortSignal;
2498
2687
  }) => Promise<BannerStatsReport>;
2688
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2689
+ abTest: (options?: {
2690
+ signal?: AbortSignal;
2691
+ }) => Promise<AbTests & {
2692
+ index: string;
2693
+ }>;
2694
+ };
2695
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2696
+ abTests: {
2697
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2698
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2699
+ shares: number[];
2700
+ days: {
2701
+ min: number;
2702
+ default: number;
2703
+ max: number;
2704
+ };
2705
+ }>;
2706
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2707
+ estimate: (options?: {
2708
+ share?: number;
2709
+ signal?: AbortSignal;
2710
+ }) => Promise<AbTestEstimate>;
2499
2711
  };
2500
2712
  upsert: (records: S_4[], signal?: AbortSignal) => Promise<WriteReceipt>;
2501
2713
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_4>>;
@@ -2702,6 +2914,29 @@ export declare class SearchServerClient {
2702
2914
  range?: "7d" | "30d" | "90d";
2703
2915
  signal?: AbortSignal;
2704
2916
  }) => Promise<BannerStatsReport>;
2917
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2918
+ abTest: (options?: {
2919
+ signal?: AbortSignal;
2920
+ }) => Promise<AbTests & {
2921
+ index: string;
2922
+ }>;
2923
+ };
2924
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2925
+ abTests: {
2926
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2927
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2928
+ shares: number[];
2929
+ days: {
2930
+ min: number;
2931
+ default: number;
2932
+ max: number;
2933
+ };
2934
+ }>;
2935
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2936
+ estimate: (options?: {
2937
+ share?: number;
2938
+ signal?: AbortSignal;
2939
+ }) => Promise<AbTestEstimate>;
2705
2940
  };
2706
2941
  upsert: (records: S_5[], signal?: AbortSignal) => Promise<WriteReceipt>;
2707
2942
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_5>>;
@@ -2908,6 +3143,29 @@ export declare class SearchServerClient {
2908
3143
  range?: "7d" | "30d" | "90d";
2909
3144
  signal?: AbortSignal;
2910
3145
  }) => Promise<BannerStatsReport>;
3146
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3147
+ abTest: (options?: {
3148
+ signal?: AbortSignal;
3149
+ }) => Promise<AbTests & {
3150
+ index: string;
3151
+ }>;
3152
+ };
3153
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3154
+ abTests: {
3155
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3156
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3157
+ shares: number[];
3158
+ days: {
3159
+ min: number;
3160
+ default: number;
3161
+ max: number;
3162
+ };
3163
+ }>;
3164
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3165
+ estimate: (options?: {
3166
+ share?: number;
3167
+ signal?: AbortSignal;
3168
+ }) => Promise<AbTestEstimate>;
2911
3169
  };
2912
3170
  upsert: (records: S_6[], signal?: AbortSignal) => Promise<WriteReceipt>;
2913
3171
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_6>>;
@@ -3114,6 +3372,29 @@ export declare class SearchServerClient {
3114
3372
  range?: "7d" | "30d" | "90d";
3115
3373
  signal?: AbortSignal;
3116
3374
  }) => Promise<BannerStatsReport>;
3375
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3376
+ abTest: (options?: {
3377
+ signal?: AbortSignal;
3378
+ }) => Promise<AbTests & {
3379
+ index: string;
3380
+ }>;
3381
+ };
3382
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3383
+ abTests: {
3384
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3385
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3386
+ shares: number[];
3387
+ days: {
3388
+ min: number;
3389
+ default: number;
3390
+ max: number;
3391
+ };
3392
+ }>;
3393
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3394
+ estimate: (options?: {
3395
+ share?: number;
3396
+ signal?: AbortSignal;
3397
+ }) => Promise<AbTestEstimate>;
3117
3398
  };
3118
3399
  upsert: (records: S_7[], signal?: AbortSignal) => Promise<WriteReceipt>;
3119
3400
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_7>>;
@@ -3320,6 +3601,29 @@ export declare class SearchServerClient {
3320
3601
  range?: "7d" | "30d" | "90d";
3321
3602
  signal?: AbortSignal;
3322
3603
  }) => Promise<BannerStatsReport>;
3604
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3605
+ abTest: (options?: {
3606
+ signal?: AbortSignal;
3607
+ }) => Promise<AbTests & {
3608
+ index: string;
3609
+ }>;
3610
+ };
3611
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3612
+ abTests: {
3613
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3614
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3615
+ shares: number[];
3616
+ days: {
3617
+ min: number;
3618
+ default: number;
3619
+ max: number;
3620
+ };
3621
+ }>;
3622
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3623
+ estimate: (options?: {
3624
+ share?: number;
3625
+ signal?: AbortSignal;
3626
+ }) => Promise<AbTestEstimate>;
3323
3627
  };
3324
3628
  upsert: (records: S_8[], signal?: AbortSignal) => Promise<WriteReceipt>;
3325
3629
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_8>>;
@@ -3526,6 +3830,29 @@ export declare class SearchServerClient {
3526
3830
  range?: "7d" | "30d" | "90d";
3527
3831
  signal?: AbortSignal;
3528
3832
  }) => Promise<BannerStatsReport>;
3833
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3834
+ abTest: (options?: {
3835
+ signal?: AbortSignal;
3836
+ }) => Promise<AbTests & {
3837
+ index: string;
3838
+ }>;
3839
+ };
3840
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3841
+ abTests: {
3842
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3843
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3844
+ shares: number[];
3845
+ days: {
3846
+ min: number;
3847
+ default: number;
3848
+ max: number;
3849
+ };
3850
+ }>;
3851
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3852
+ estimate: (options?: {
3853
+ share?: number;
3854
+ signal?: AbortSignal;
3855
+ }) => Promise<AbTestEstimate>;
3529
3856
  };
3530
3857
  upsert: (records: S_9[], signal?: AbortSignal) => Promise<WriteReceipt>;
3531
3858
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_9>>;
package/dist/index.js CHANGED
@@ -194,7 +194,8 @@ class SearchServerClient {
194
194
  return jsonResponse(await response.text(), response.status, retryAfter ? { "retry-after": retryAfter } : {});
195
195
  };
196
196
  const sendEvents = async (events, options = {}) => {
197
- const { signal, visitorId, visitId } = options;
197
+ const { signal, visitorId, visitId, abTest } = options;
198
+ const label = abTest === undefined || typeof abTest === "string" ? abTest : `${abTest.id}:${abTest.variant}`;
198
199
  const result = { accepted: 0, rejected: [] };
199
200
  let batch = [];
200
201
  let sent = 0;
@@ -204,6 +205,7 @@ class SearchServerClient {
204
205
  const answer = await postEvents({
205
206
  ...visitorId === undefined ? {} : { visitorId },
206
207
  ...visitId === undefined ? {} : { visitId },
208
+ ...label === undefined ? {} : { abTest: label },
207
209
  events: batch
208
210
  }, signal);
209
211
  result.accepted += answer.accepted ?? 0;
@@ -347,7 +349,12 @@ class SearchServerClient {
347
349
  product: (objectID, options = {}) => report(`products/${encodeURIComponent(objectID)}`, options),
348
350
  revenue: (options = {}) => report("revenue", options),
349
351
  filterSales: (options = {}) => report("filter-sales", options),
350
- banners: (options = {}) => request(`/banners?range=${options.range ?? "30d"}`, "GET", undefined, options.signal)
352
+ banners: (options = {}) => request(`/banners?range=${options.range ?? "30d"}`, "GET", undefined, options.signal),
353
+ abTest: (options = {}) => request("/analytics/ab-test", "GET", undefined, options.signal)
354
+ },
355
+ abTests: {
356
+ get: (signal) => request("/ab-tests", "GET", undefined, signal),
357
+ estimate: (options = {}) => request(`/ab-tests/estimate${options.share === undefined ? "" : `?share=${options.share}`}`, "GET", undefined, options.signal)
351
358
  },
352
359
  upsert: (records, signal) => request("/records", "POST", { records }, signal),
353
360
  getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@findlane/server",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Findlane server SDK: upload catalogs, manage index settings, and search with a secret workspace API key.",
5
5
  "keywords": ["findlane", "search", "full-text search", "typo tolerance", "ecommerce search", "search api", "sdk"],
6
6
  "license": "MIT",