@findlane/server 0.31.0 → 0.33.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
@@ -539,6 +539,9 @@ export type SearchResponse<T> = {
539
539
  };
540
540
  /** A rule pinned it at its place (0.20.0). */
541
541
  pinned?: true;
542
+ /** With optimized images on (0.33.0): its image's placeholder, a ThumbHash in base64, for the browser SDK's
543
+ * `Image` (its `placeholder` prop) to show while the image loads. */
544
+ placeholder?: string;
542
545
  }[];
543
546
  /** The matches; with the index's `distinct` setting, the groups. */
544
547
  total: number;
@@ -974,6 +977,80 @@ export type EventOptions = {
974
977
  signal?: AbortSignal;
975
978
  visitorId?: string;
976
979
  visitId?: string;
980
+ /** The shopper's group in the index's running A/B test (0.32.0): the `abTest` of the search answer their browser got,
981
+ * as `{ id, variant }` or `"<id>:<variant>"`, which the browser SDK sends with its own events. Your server's events
982
+ * count toward a group only with it, so send one shopper's events a call. A label for a test that ended is ignored. */
983
+ abTest?: AbTestLabel | string;
984
+ };
985
+ /** A shopper's group in an A/B test, as a browser search answer's `abTest` gives it: `a` searched with today's
986
+ * settings, `b` with the change (0.32.0). */
987
+ export type AbTestLabel = {
988
+ id: string;
989
+ variant: "a" | "b";
990
+ };
991
+ /** One measure in an A/B test's results: each group's value per search (a share for `noResults` and `topThree`), and
992
+ * B's change over A (0.1 is 10% more) with its 95% interval. Null where there aren't the searches or days yet. */
993
+ export type AbFigure = {
994
+ a: number | null;
995
+ b: number | null;
996
+ change: number | null;
997
+ low: number | null;
998
+ high: number | null;
999
+ };
1000
+ /** An A/B test's results. `decided` once the test has run its days with 1,000 searches in each group; `verdict` is then
1001
+ * `b` (the change did better), `a` (worse) or `none` (no clear difference), judged on clicks per search, then carts. */
1002
+ export type AbTestResults = {
1003
+ days: number;
1004
+ decided: boolean;
1005
+ verdict: "a" | "b" | "none" | null;
1006
+ searches: {
1007
+ a: number;
1008
+ b: number;
1009
+ };
1010
+ currency: string | null;
1011
+ figures: Record<"noResults" | "clicks" | "topThree" | "carts" | "orders" | "revenue", AbFigure>;
1012
+ perDay: {
1013
+ day: string;
1014
+ searches: {
1015
+ a: number;
1016
+ b: number;
1017
+ };
1018
+ clicks: {
1019
+ a: number;
1020
+ b: number;
1021
+ };
1022
+ }[];
1023
+ computedAt: string;
1024
+ };
1025
+ /** 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
1026
+ * in `patch` (null where B goes without one), an A/A test (`aa`) changes none. `outcome` once ended: `a` kept today's
1027
+ * settings, `b` applied the change, `guard` ended early because the change found nothing far more often. */
1028
+ export type AbTest = {
1029
+ id: string;
1030
+ name: string;
1031
+ share: number;
1032
+ changes: string[];
1033
+ aa: boolean;
1034
+ patch: Record<string, unknown>;
1035
+ startedAt: string;
1036
+ endsAt: string;
1037
+ endedAt: string | null;
1038
+ outcome: "a" | "b" | "guard" | null;
1039
+ results: AbTestResults | null;
1040
+ };
1041
+ /** An index's A/B tests: the running one with its results so far, or null, and up to 10 that ended, newest first. */
1042
+ export type AbTests = {
1043
+ running: AbTest | null;
1044
+ ended: AbTest[];
1045
+ };
1046
+ /** What a test at a share would take: the days to find a 10% change in clicks per search (null with too few
1047
+ * searches), from the index's searches and clicks per search over the last 14 days, and the share of searches more
1048
+ * that would miss the cache. */
1049
+ export type AbTestEstimate = {
1050
+ searchesPerDay: number;
1051
+ clicksPerSearch: number;
1052
+ days: number | null;
1053
+ extraMisses: number;
977
1054
  };
978
1055
  /** An analytics window, in UTC: `range` (7d by default), or `from` and `to`. A date as `from` is the start of
979
1056
  * that day, and as `to` includes the whole day; `to` is now by default. Analytics go back 90 days: a window
@@ -1464,6 +1541,29 @@ export declare class SearchServerClient {
1464
1541
  range?: "7d" | "30d" | "90d";
1465
1542
  signal?: AbortSignal;
1466
1543
  }) => Promise<BannerStatsReport>;
1544
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
1545
+ abTest: (options?: {
1546
+ signal?: AbortSignal;
1547
+ }) => Promise<AbTests & {
1548
+ index: string;
1549
+ }>;
1550
+ };
1551
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
1552
+ abTests: {
1553
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
1554
+ get: (signal?: AbortSignal) => Promise<AbTests & {
1555
+ shares: number[];
1556
+ days: {
1557
+ min: number;
1558
+ default: number;
1559
+ max: number;
1560
+ };
1561
+ }>;
1562
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
1563
+ estimate: (options?: {
1564
+ share?: number;
1565
+ signal?: AbortSignal;
1566
+ }) => Promise<AbTestEstimate>;
1467
1567
  };
1468
1568
  upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
1469
1569
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
@@ -1672,6 +1772,29 @@ export declare class SearchServerClient {
1672
1772
  range?: "7d" | "30d" | "90d";
1673
1773
  signal?: AbortSignal;
1674
1774
  }) => Promise<BannerStatsReport>;
1775
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
1776
+ abTest: (options?: {
1777
+ signal?: AbortSignal;
1778
+ }) => Promise<AbTests & {
1779
+ index: string;
1780
+ }>;
1781
+ };
1782
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
1783
+ abTests: {
1784
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
1785
+ get: (signal?: AbortSignal) => Promise<AbTests & {
1786
+ shares: number[];
1787
+ days: {
1788
+ min: number;
1789
+ default: number;
1790
+ max: number;
1791
+ };
1792
+ }>;
1793
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
1794
+ estimate: (options?: {
1795
+ share?: number;
1796
+ signal?: AbortSignal;
1797
+ }) => Promise<AbTestEstimate>;
1675
1798
  };
1676
1799
  upsert: (records: S[], signal?: AbortSignal) => Promise<WriteReceipt>;
1677
1800
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S>>;
@@ -1878,6 +2001,29 @@ export declare class SearchServerClient {
1878
2001
  range?: "7d" | "30d" | "90d";
1879
2002
  signal?: AbortSignal;
1880
2003
  }) => Promise<BannerStatsReport>;
2004
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2005
+ abTest: (options?: {
2006
+ signal?: AbortSignal;
2007
+ }) => Promise<AbTests & {
2008
+ index: string;
2009
+ }>;
2010
+ };
2011
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2012
+ abTests: {
2013
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2014
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2015
+ shares: number[];
2016
+ days: {
2017
+ min: number;
2018
+ default: number;
2019
+ max: number;
2020
+ };
2021
+ }>;
2022
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2023
+ estimate: (options?: {
2024
+ share?: number;
2025
+ signal?: AbortSignal;
2026
+ }) => Promise<AbTestEstimate>;
1881
2027
  };
1882
2028
  upsert: (records: S_1[], signal?: AbortSignal) => Promise<WriteReceipt>;
1883
2029
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_1>>;
@@ -2084,6 +2230,29 @@ export declare class SearchServerClient {
2084
2230
  range?: "7d" | "30d" | "90d";
2085
2231
  signal?: AbortSignal;
2086
2232
  }) => Promise<BannerStatsReport>;
2233
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2234
+ abTest: (options?: {
2235
+ signal?: AbortSignal;
2236
+ }) => Promise<AbTests & {
2237
+ index: string;
2238
+ }>;
2239
+ };
2240
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2241
+ abTests: {
2242
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2243
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2244
+ shares: number[];
2245
+ days: {
2246
+ min: number;
2247
+ default: number;
2248
+ max: number;
2249
+ };
2250
+ }>;
2251
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2252
+ estimate: (options?: {
2253
+ share?: number;
2254
+ signal?: AbortSignal;
2255
+ }) => Promise<AbTestEstimate>;
2087
2256
  };
2088
2257
  upsert: (records: S_2[], signal?: AbortSignal) => Promise<WriteReceipt>;
2089
2258
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_2>>;
@@ -2290,6 +2459,29 @@ export declare class SearchServerClient {
2290
2459
  range?: "7d" | "30d" | "90d";
2291
2460
  signal?: AbortSignal;
2292
2461
  }) => Promise<BannerStatsReport>;
2462
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2463
+ abTest: (options?: {
2464
+ signal?: AbortSignal;
2465
+ }) => Promise<AbTests & {
2466
+ index: string;
2467
+ }>;
2468
+ };
2469
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2470
+ abTests: {
2471
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2472
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2473
+ shares: number[];
2474
+ days: {
2475
+ min: number;
2476
+ default: number;
2477
+ max: number;
2478
+ };
2479
+ }>;
2480
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2481
+ estimate: (options?: {
2482
+ share?: number;
2483
+ signal?: AbortSignal;
2484
+ }) => Promise<AbTestEstimate>;
2293
2485
  };
2294
2486
  upsert: (records: S_3[], signal?: AbortSignal) => Promise<WriteReceipt>;
2295
2487
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_3>>;
@@ -2496,6 +2688,29 @@ export declare class SearchServerClient {
2496
2688
  range?: "7d" | "30d" | "90d";
2497
2689
  signal?: AbortSignal;
2498
2690
  }) => Promise<BannerStatsReport>;
2691
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2692
+ abTest: (options?: {
2693
+ signal?: AbortSignal;
2694
+ }) => Promise<AbTests & {
2695
+ index: string;
2696
+ }>;
2697
+ };
2698
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2699
+ abTests: {
2700
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2701
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2702
+ shares: number[];
2703
+ days: {
2704
+ min: number;
2705
+ default: number;
2706
+ max: number;
2707
+ };
2708
+ }>;
2709
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2710
+ estimate: (options?: {
2711
+ share?: number;
2712
+ signal?: AbortSignal;
2713
+ }) => Promise<AbTestEstimate>;
2499
2714
  };
2500
2715
  upsert: (records: S_4[], signal?: AbortSignal) => Promise<WriteReceipt>;
2501
2716
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_4>>;
@@ -2702,6 +2917,29 @@ export declare class SearchServerClient {
2702
2917
  range?: "7d" | "30d" | "90d";
2703
2918
  signal?: AbortSignal;
2704
2919
  }) => Promise<BannerStatsReport>;
2920
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2921
+ abTest: (options?: {
2922
+ signal?: AbortSignal;
2923
+ }) => Promise<AbTests & {
2924
+ index: string;
2925
+ }>;
2926
+ };
2927
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2928
+ abTests: {
2929
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2930
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2931
+ shares: number[];
2932
+ days: {
2933
+ min: number;
2934
+ default: number;
2935
+ max: number;
2936
+ };
2937
+ }>;
2938
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2939
+ estimate: (options?: {
2940
+ share?: number;
2941
+ signal?: AbortSignal;
2942
+ }) => Promise<AbTestEstimate>;
2705
2943
  };
2706
2944
  upsert: (records: S_5[], signal?: AbortSignal) => Promise<WriteReceipt>;
2707
2945
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_5>>;
@@ -2908,6 +3146,29 @@ export declare class SearchServerClient {
2908
3146
  range?: "7d" | "30d" | "90d";
2909
3147
  signal?: AbortSignal;
2910
3148
  }) => Promise<BannerStatsReport>;
3149
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3150
+ abTest: (options?: {
3151
+ signal?: AbortSignal;
3152
+ }) => Promise<AbTests & {
3153
+ index: string;
3154
+ }>;
3155
+ };
3156
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3157
+ abTests: {
3158
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3159
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3160
+ shares: number[];
3161
+ days: {
3162
+ min: number;
3163
+ default: number;
3164
+ max: number;
3165
+ };
3166
+ }>;
3167
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3168
+ estimate: (options?: {
3169
+ share?: number;
3170
+ signal?: AbortSignal;
3171
+ }) => Promise<AbTestEstimate>;
2911
3172
  };
2912
3173
  upsert: (records: S_6[], signal?: AbortSignal) => Promise<WriteReceipt>;
2913
3174
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_6>>;
@@ -3114,6 +3375,29 @@ export declare class SearchServerClient {
3114
3375
  range?: "7d" | "30d" | "90d";
3115
3376
  signal?: AbortSignal;
3116
3377
  }) => Promise<BannerStatsReport>;
3378
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3379
+ abTest: (options?: {
3380
+ signal?: AbortSignal;
3381
+ }) => Promise<AbTests & {
3382
+ index: string;
3383
+ }>;
3384
+ };
3385
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3386
+ abTests: {
3387
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3388
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3389
+ shares: number[];
3390
+ days: {
3391
+ min: number;
3392
+ default: number;
3393
+ max: number;
3394
+ };
3395
+ }>;
3396
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3397
+ estimate: (options?: {
3398
+ share?: number;
3399
+ signal?: AbortSignal;
3400
+ }) => Promise<AbTestEstimate>;
3117
3401
  };
3118
3402
  upsert: (records: S_7[], signal?: AbortSignal) => Promise<WriteReceipt>;
3119
3403
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_7>>;
@@ -3320,6 +3604,29 @@ export declare class SearchServerClient {
3320
3604
  range?: "7d" | "30d" | "90d";
3321
3605
  signal?: AbortSignal;
3322
3606
  }) => Promise<BannerStatsReport>;
3607
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3608
+ abTest: (options?: {
3609
+ signal?: AbortSignal;
3610
+ }) => Promise<AbTests & {
3611
+ index: string;
3612
+ }>;
3613
+ };
3614
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3615
+ abTests: {
3616
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3617
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3618
+ shares: number[];
3619
+ days: {
3620
+ min: number;
3621
+ default: number;
3622
+ max: number;
3623
+ };
3624
+ }>;
3625
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3626
+ estimate: (options?: {
3627
+ share?: number;
3628
+ signal?: AbortSignal;
3629
+ }) => Promise<AbTestEstimate>;
3323
3630
  };
3324
3631
  upsert: (records: S_8[], signal?: AbortSignal) => Promise<WriteReceipt>;
3325
3632
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_8>>;
@@ -3526,6 +3833,29 @@ export declare class SearchServerClient {
3526
3833
  range?: "7d" | "30d" | "90d";
3527
3834
  signal?: AbortSignal;
3528
3835
  }) => Promise<BannerStatsReport>;
3836
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3837
+ abTest: (options?: {
3838
+ signal?: AbortSignal;
3839
+ }) => Promise<AbTests & {
3840
+ index: string;
3841
+ }>;
3842
+ };
3843
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3844
+ abTests: {
3845
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3846
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3847
+ shares: number[];
3848
+ days: {
3849
+ min: number;
3850
+ default: number;
3851
+ max: number;
3852
+ };
3853
+ }>;
3854
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3855
+ estimate: (options?: {
3856
+ share?: number;
3857
+ signal?: AbortSignal;
3858
+ }) => Promise<AbTestEstimate>;
3529
3859
  };
3530
3860
  upsert: (records: S_9[], signal?: AbortSignal) => Promise<WriteReceipt>;
3531
3861
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_9>>;
package/dist/index.d.ts CHANGED
@@ -539,6 +539,9 @@ export type SearchResponse<T> = {
539
539
  };
540
540
  /** A rule pinned it at its place (0.20.0). */
541
541
  pinned?: true;
542
+ /** With optimized images on (0.33.0): its image's placeholder, a ThumbHash in base64, for the browser SDK's
543
+ * `Image` (its `placeholder` prop) to show while the image loads. */
544
+ placeholder?: string;
542
545
  }[];
543
546
  /** The matches; with the index's `distinct` setting, the groups. */
544
547
  total: number;
@@ -974,6 +977,80 @@ export type EventOptions = {
974
977
  signal?: AbortSignal;
975
978
  visitorId?: string;
976
979
  visitId?: string;
980
+ /** The shopper's group in the index's running A/B test (0.32.0): the `abTest` of the search answer their browser got,
981
+ * as `{ id, variant }` or `"<id>:<variant>"`, which the browser SDK sends with its own events. Your server's events
982
+ * count toward a group only with it, so send one shopper's events a call. A label for a test that ended is ignored. */
983
+ abTest?: AbTestLabel | string;
984
+ };
985
+ /** A shopper's group in an A/B test, as a browser search answer's `abTest` gives it: `a` searched with today's
986
+ * settings, `b` with the change (0.32.0). */
987
+ export type AbTestLabel = {
988
+ id: string;
989
+ variant: "a" | "b";
990
+ };
991
+ /** One measure in an A/B test's results: each group's value per search (a share for `noResults` and `topThree`), and
992
+ * B's change over A (0.1 is 10% more) with its 95% interval. Null where there aren't the searches or days yet. */
993
+ export type AbFigure = {
994
+ a: number | null;
995
+ b: number | null;
996
+ change: number | null;
997
+ low: number | null;
998
+ high: number | null;
999
+ };
1000
+ /** An A/B test's results. `decided` once the test has run its days with 1,000 searches in each group; `verdict` is then
1001
+ * `b` (the change did better), `a` (worse) or `none` (no clear difference), judged on clicks per search, then carts. */
1002
+ export type AbTestResults = {
1003
+ days: number;
1004
+ decided: boolean;
1005
+ verdict: "a" | "b" | "none" | null;
1006
+ searches: {
1007
+ a: number;
1008
+ b: number;
1009
+ };
1010
+ currency: string | null;
1011
+ figures: Record<"noResults" | "clicks" | "topThree" | "carts" | "orders" | "revenue", AbFigure>;
1012
+ perDay: {
1013
+ day: string;
1014
+ searches: {
1015
+ a: number;
1016
+ b: number;
1017
+ };
1018
+ clicks: {
1019
+ a: number;
1020
+ b: number;
1021
+ };
1022
+ }[];
1023
+ computedAt: string;
1024
+ };
1025
+ /** 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
1026
+ * in `patch` (null where B goes without one), an A/A test (`aa`) changes none. `outcome` once ended: `a` kept today's
1027
+ * settings, `b` applied the change, `guard` ended early because the change found nothing far more often. */
1028
+ export type AbTest = {
1029
+ id: string;
1030
+ name: string;
1031
+ share: number;
1032
+ changes: string[];
1033
+ aa: boolean;
1034
+ patch: Record<string, unknown>;
1035
+ startedAt: string;
1036
+ endsAt: string;
1037
+ endedAt: string | null;
1038
+ outcome: "a" | "b" | "guard" | null;
1039
+ results: AbTestResults | null;
1040
+ };
1041
+ /** An index's A/B tests: the running one with its results so far, or null, and up to 10 that ended, newest first. */
1042
+ export type AbTests = {
1043
+ running: AbTest | null;
1044
+ ended: AbTest[];
1045
+ };
1046
+ /** What a test at a share would take: the days to find a 10% change in clicks per search (null with too few
1047
+ * searches), from the index's searches and clicks per search over the last 14 days, and the share of searches more
1048
+ * that would miss the cache. */
1049
+ export type AbTestEstimate = {
1050
+ searchesPerDay: number;
1051
+ clicksPerSearch: number;
1052
+ days: number | null;
1053
+ extraMisses: number;
977
1054
  };
978
1055
  /** An analytics window, in UTC: `range` (7d by default), or `from` and `to`. A date as `from` is the start of
979
1056
  * that day, and as `to` includes the whole day; `to` is now by default. Analytics go back 90 days: a window
@@ -1464,6 +1541,29 @@ export declare class SearchServerClient {
1464
1541
  range?: "7d" | "30d" | "90d";
1465
1542
  signal?: AbortSignal;
1466
1543
  }) => Promise<BannerStatsReport>;
1544
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
1545
+ abTest: (options?: {
1546
+ signal?: AbortSignal;
1547
+ }) => Promise<AbTests & {
1548
+ index: string;
1549
+ }>;
1550
+ };
1551
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
1552
+ abTests: {
1553
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
1554
+ get: (signal?: AbortSignal) => Promise<AbTests & {
1555
+ shares: number[];
1556
+ days: {
1557
+ min: number;
1558
+ default: number;
1559
+ max: number;
1560
+ };
1561
+ }>;
1562
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
1563
+ estimate: (options?: {
1564
+ share?: number;
1565
+ signal?: AbortSignal;
1566
+ }) => Promise<AbTestEstimate>;
1467
1567
  };
1468
1568
  upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
1469
1569
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
@@ -1672,6 +1772,29 @@ export declare class SearchServerClient {
1672
1772
  range?: "7d" | "30d" | "90d";
1673
1773
  signal?: AbortSignal;
1674
1774
  }) => Promise<BannerStatsReport>;
1775
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
1776
+ abTest: (options?: {
1777
+ signal?: AbortSignal;
1778
+ }) => Promise<AbTests & {
1779
+ index: string;
1780
+ }>;
1781
+ };
1782
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
1783
+ abTests: {
1784
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
1785
+ get: (signal?: AbortSignal) => Promise<AbTests & {
1786
+ shares: number[];
1787
+ days: {
1788
+ min: number;
1789
+ default: number;
1790
+ max: number;
1791
+ };
1792
+ }>;
1793
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
1794
+ estimate: (options?: {
1795
+ share?: number;
1796
+ signal?: AbortSignal;
1797
+ }) => Promise<AbTestEstimate>;
1675
1798
  };
1676
1799
  upsert: (records: S[], signal?: AbortSignal) => Promise<WriteReceipt>;
1677
1800
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S>>;
@@ -1878,6 +2001,29 @@ export declare class SearchServerClient {
1878
2001
  range?: "7d" | "30d" | "90d";
1879
2002
  signal?: AbortSignal;
1880
2003
  }) => Promise<BannerStatsReport>;
2004
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2005
+ abTest: (options?: {
2006
+ signal?: AbortSignal;
2007
+ }) => Promise<AbTests & {
2008
+ index: string;
2009
+ }>;
2010
+ };
2011
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2012
+ abTests: {
2013
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2014
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2015
+ shares: number[];
2016
+ days: {
2017
+ min: number;
2018
+ default: number;
2019
+ max: number;
2020
+ };
2021
+ }>;
2022
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2023
+ estimate: (options?: {
2024
+ share?: number;
2025
+ signal?: AbortSignal;
2026
+ }) => Promise<AbTestEstimate>;
1881
2027
  };
1882
2028
  upsert: (records: S_1[], signal?: AbortSignal) => Promise<WriteReceipt>;
1883
2029
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_1>>;
@@ -2084,6 +2230,29 @@ export declare class SearchServerClient {
2084
2230
  range?: "7d" | "30d" | "90d";
2085
2231
  signal?: AbortSignal;
2086
2232
  }) => Promise<BannerStatsReport>;
2233
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2234
+ abTest: (options?: {
2235
+ signal?: AbortSignal;
2236
+ }) => Promise<AbTests & {
2237
+ index: string;
2238
+ }>;
2239
+ };
2240
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2241
+ abTests: {
2242
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2243
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2244
+ shares: number[];
2245
+ days: {
2246
+ min: number;
2247
+ default: number;
2248
+ max: number;
2249
+ };
2250
+ }>;
2251
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2252
+ estimate: (options?: {
2253
+ share?: number;
2254
+ signal?: AbortSignal;
2255
+ }) => Promise<AbTestEstimate>;
2087
2256
  };
2088
2257
  upsert: (records: S_2[], signal?: AbortSignal) => Promise<WriteReceipt>;
2089
2258
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_2>>;
@@ -2290,6 +2459,29 @@ export declare class SearchServerClient {
2290
2459
  range?: "7d" | "30d" | "90d";
2291
2460
  signal?: AbortSignal;
2292
2461
  }) => Promise<BannerStatsReport>;
2462
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2463
+ abTest: (options?: {
2464
+ signal?: AbortSignal;
2465
+ }) => Promise<AbTests & {
2466
+ index: string;
2467
+ }>;
2468
+ };
2469
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2470
+ abTests: {
2471
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2472
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2473
+ shares: number[];
2474
+ days: {
2475
+ min: number;
2476
+ default: number;
2477
+ max: number;
2478
+ };
2479
+ }>;
2480
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2481
+ estimate: (options?: {
2482
+ share?: number;
2483
+ signal?: AbortSignal;
2484
+ }) => Promise<AbTestEstimate>;
2293
2485
  };
2294
2486
  upsert: (records: S_3[], signal?: AbortSignal) => Promise<WriteReceipt>;
2295
2487
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_3>>;
@@ -2496,6 +2688,29 @@ export declare class SearchServerClient {
2496
2688
  range?: "7d" | "30d" | "90d";
2497
2689
  signal?: AbortSignal;
2498
2690
  }) => Promise<BannerStatsReport>;
2691
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2692
+ abTest: (options?: {
2693
+ signal?: AbortSignal;
2694
+ }) => Promise<AbTests & {
2695
+ index: string;
2696
+ }>;
2697
+ };
2698
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2699
+ abTests: {
2700
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2701
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2702
+ shares: number[];
2703
+ days: {
2704
+ min: number;
2705
+ default: number;
2706
+ max: number;
2707
+ };
2708
+ }>;
2709
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2710
+ estimate: (options?: {
2711
+ share?: number;
2712
+ signal?: AbortSignal;
2713
+ }) => Promise<AbTestEstimate>;
2499
2714
  };
2500
2715
  upsert: (records: S_4[], signal?: AbortSignal) => Promise<WriteReceipt>;
2501
2716
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_4>>;
@@ -2702,6 +2917,29 @@ export declare class SearchServerClient {
2702
2917
  range?: "7d" | "30d" | "90d";
2703
2918
  signal?: AbortSignal;
2704
2919
  }) => Promise<BannerStatsReport>;
2920
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
2921
+ abTest: (options?: {
2922
+ signal?: AbortSignal;
2923
+ }) => Promise<AbTests & {
2924
+ index: string;
2925
+ }>;
2926
+ };
2927
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
2928
+ abTests: {
2929
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
2930
+ get: (signal?: AbortSignal) => Promise<AbTests & {
2931
+ shares: number[];
2932
+ days: {
2933
+ min: number;
2934
+ default: number;
2935
+ max: number;
2936
+ };
2937
+ }>;
2938
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
2939
+ estimate: (options?: {
2940
+ share?: number;
2941
+ signal?: AbortSignal;
2942
+ }) => Promise<AbTestEstimate>;
2705
2943
  };
2706
2944
  upsert: (records: S_5[], signal?: AbortSignal) => Promise<WriteReceipt>;
2707
2945
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_5>>;
@@ -2908,6 +3146,29 @@ export declare class SearchServerClient {
2908
3146
  range?: "7d" | "30d" | "90d";
2909
3147
  signal?: AbortSignal;
2910
3148
  }) => Promise<BannerStatsReport>;
3149
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3150
+ abTest: (options?: {
3151
+ signal?: AbortSignal;
3152
+ }) => Promise<AbTests & {
3153
+ index: string;
3154
+ }>;
3155
+ };
3156
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3157
+ abTests: {
3158
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3159
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3160
+ shares: number[];
3161
+ days: {
3162
+ min: number;
3163
+ default: number;
3164
+ max: number;
3165
+ };
3166
+ }>;
3167
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3168
+ estimate: (options?: {
3169
+ share?: number;
3170
+ signal?: AbortSignal;
3171
+ }) => Promise<AbTestEstimate>;
2911
3172
  };
2912
3173
  upsert: (records: S_6[], signal?: AbortSignal) => Promise<WriteReceipt>;
2913
3174
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_6>>;
@@ -3114,6 +3375,29 @@ export declare class SearchServerClient {
3114
3375
  range?: "7d" | "30d" | "90d";
3115
3376
  signal?: AbortSignal;
3116
3377
  }) => Promise<BannerStatsReport>;
3378
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3379
+ abTest: (options?: {
3380
+ signal?: AbortSignal;
3381
+ }) => Promise<AbTests & {
3382
+ index: string;
3383
+ }>;
3384
+ };
3385
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3386
+ abTests: {
3387
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3388
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3389
+ shares: number[];
3390
+ days: {
3391
+ min: number;
3392
+ default: number;
3393
+ max: number;
3394
+ };
3395
+ }>;
3396
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3397
+ estimate: (options?: {
3398
+ share?: number;
3399
+ signal?: AbortSignal;
3400
+ }) => Promise<AbTestEstimate>;
3117
3401
  };
3118
3402
  upsert: (records: S_7[], signal?: AbortSignal) => Promise<WriteReceipt>;
3119
3403
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_7>>;
@@ -3320,6 +3604,29 @@ export declare class SearchServerClient {
3320
3604
  range?: "7d" | "30d" | "90d";
3321
3605
  signal?: AbortSignal;
3322
3606
  }) => Promise<BannerStatsReport>;
3607
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3608
+ abTest: (options?: {
3609
+ signal?: AbortSignal;
3610
+ }) => Promise<AbTests & {
3611
+ index: string;
3612
+ }>;
3613
+ };
3614
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3615
+ abTests: {
3616
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3617
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3618
+ shares: number[];
3619
+ days: {
3620
+ min: number;
3621
+ default: number;
3622
+ max: number;
3623
+ };
3624
+ }>;
3625
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3626
+ estimate: (options?: {
3627
+ share?: number;
3628
+ signal?: AbortSignal;
3629
+ }) => Promise<AbTestEstimate>;
3323
3630
  };
3324
3631
  upsert: (records: S_8[], signal?: AbortSignal) => Promise<WriteReceipt>;
3325
3632
  getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<S_8>>;
@@ -3526,6 +3833,29 @@ export declare class SearchServerClient {
3526
3833
  range?: "7d" | "30d" | "90d";
3527
3834
  signal?: AbortSignal;
3528
3835
  }) => Promise<BannerStatsReport>;
3836
+ /** The index's A/B tests, as `abTests.get()` reads them with a search or write key (0.32.0). */
3837
+ abTest: (options?: {
3838
+ signal?: AbortSignal;
3839
+ }) => Promise<AbTests & {
3840
+ index: string;
3841
+ }>;
3842
+ };
3843
+ /** The index's A/B tests of its settings (0.32.0); owners start and end them in the dashboard, or through an agent. */
3844
+ abTests: {
3845
+ /** The running test with its results so far, and those that ended; with the shares and days a test may take. */
3846
+ get: (signal?: AbortSignal) => Promise<AbTests & {
3847
+ shares: number[];
3848
+ days: {
3849
+ min: number;
3850
+ default: number;
3851
+ max: number;
3852
+ };
3853
+ }>;
3854
+ /** What a test would take at a `share` of shoppers in group B: 0.1 to 0.5, 0.5 by default. */
3855
+ estimate: (options?: {
3856
+ share?: number;
3857
+ signal?: AbortSignal;
3858
+ }) => Promise<AbTestEstimate>;
3529
3859
  };
3530
3860
  upsert: (records: S_9[], signal?: AbortSignal) => Promise<WriteReceipt>;
3531
3861
  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.33.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",