@knocklabs/client 0.20.4 → 0.21.1

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 (33) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/cjs/api.js +1 -1
  3. package/dist/cjs/clients/feed/feed.js +1 -1
  4. package/dist/cjs/clients/feed/feed.js.map +1 -1
  5. package/dist/cjs/clients/feed/utils.js.map +1 -1
  6. package/dist/cjs/clients/guide/client.js +1 -1
  7. package/dist/cjs/clients/guide/client.js.map +1 -1
  8. package/dist/cjs/index.js +1 -1
  9. package/dist/esm/api.mjs +1 -1
  10. package/dist/esm/clients/feed/feed.mjs +2 -1
  11. package/dist/esm/clients/feed/feed.mjs.map +1 -1
  12. package/dist/esm/clients/feed/utils.mjs.map +1 -1
  13. package/dist/esm/clients/guide/client.mjs +212 -190
  14. package/dist/esm/clients/guide/client.mjs.map +1 -1
  15. package/dist/esm/index.mjs +11 -10
  16. package/dist/types/clients/feed/feed.d.ts.map +1 -1
  17. package/dist/types/clients/feed/interfaces.d.ts +35 -3
  18. package/dist/types/clients/feed/interfaces.d.ts.map +1 -1
  19. package/dist/types/clients/feed/utils.d.ts +1 -0
  20. package/dist/types/clients/feed/utils.d.ts.map +1 -1
  21. package/dist/types/clients/guide/client.d.ts +4 -1
  22. package/dist/types/clients/guide/client.d.ts.map +1 -1
  23. package/dist/types/clients/guide/index.d.ts +2 -2
  24. package/dist/types/clients/guide/index.d.ts.map +1 -1
  25. package/dist/types/clients/guide/types.d.ts +12 -1
  26. package/dist/types/clients/guide/types.d.ts.map +1 -1
  27. package/package.json +2 -2
  28. package/src/clients/feed/feed.ts +2 -1
  29. package/src/clients/feed/interfaces.ts +35 -6
  30. package/src/clients/feed/utils.ts +11 -2
  31. package/src/clients/guide/client.ts +89 -52
  32. package/src/clients/guide/index.ts +7 -1
  33. package/src/clients/guide/types.ts +21 -1
@@ -48,6 +48,16 @@ export interface FeedClientOptions {
48
48
  // Optionally set whether to be inclusive of the start and end dates
49
49
  inclusive?: boolean;
50
50
  };
51
+ /**
52
+ * The mode to render the feed items in. When `mode` is `compact`:
53
+ *
54
+ * - The `activities` and `total_activities` fields will _not_ be present on feed items
55
+ * - The `data` field will _not_ include nested arrays and objects
56
+ * - The `actors` field will only have up to one actor
57
+ *
58
+ * @default "compact"
59
+ */
60
+ mode?: "rich" | "compact";
51
61
  }
52
62
 
53
63
  export type FetchFeedOptions = {
@@ -55,14 +65,25 @@ export type FetchFeedOptions = {
55
65
  __fetchSource?: "socket" | "http";
56
66
  } & Omit<FeedClientOptions, "__experimentalCrossBrowserUpdates">;
57
67
 
58
- // The final data shape that is sent to the API
59
- // Should match types here: https://docs.knock.app/reference#get-feed
68
+ /**
69
+ * The final data shape that is sent to the the list feed items endpoint of the Knock API.
70
+ *
71
+ * @see https://docs.knock.app/api-reference/users/feeds/list_items
72
+ */
60
73
  export type FetchFeedOptionsForRequest = Omit<
61
74
  FeedClientOptions,
62
- "trigger_data"
75
+ "trigger_data" | "inserted_at_date_range"
63
76
  > & {
64
- // Formatted trigger data into a string
77
+ /** The trigger data of the feed items (as a JSON string). */
65
78
  trigger_data?: string;
79
+ /** Limits the results to items inserted after or on the given date. */
80
+ "inserted_at.gte"?: string;
81
+ /** Limits the results to items inserted before or on the given date. */
82
+ "inserted_at.lte"?: string;
83
+ /** Limits the results to items inserted after the given date. */
84
+ "inserted_at.gt"?: string;
85
+ /** Limits the results to items inserted before the given date. */
86
+ "inserted_at.lt"?: string;
66
87
  // Unset options that should not be sent to the API
67
88
  __loadingType: undefined;
68
89
  __fetchSource: undefined;
@@ -107,7 +128,11 @@ export type ContentBlock =
107
128
  export interface FeedItem<T = GenericData> {
108
129
  __cursor: string;
109
130
  id: string;
110
- activities: Activity<T>[];
131
+ /**
132
+ * List of activities associated with this feed item.
133
+ * Only present in "rich" mode.
134
+ */
135
+ activities?: Activity<T>[];
111
136
  actors: Recipient[];
112
137
  blocks: ContentBlock[];
113
138
  inserted_at: string;
@@ -118,7 +143,11 @@ export interface FeedItem<T = GenericData> {
118
143
  interacted_at: string | null;
119
144
  link_clicked_at: string | null;
120
145
  archived_at: string | null;
121
- total_activities: number;
146
+ /**
147
+ * Total number of activities related to this feed item.
148
+ * Only present in "rich" mode.
149
+ */
150
+ total_activities?: number;
122
151
  total_actors: number;
123
152
  data: T | null;
124
153
  source: NotificationSource;
@@ -1,4 +1,8 @@
1
- import type { FeedClientOptions, FeedItem } from "./interfaces";
1
+ import type {
2
+ FeedClientOptions,
3
+ FeedItem,
4
+ FetchFeedOptionsForRequest,
5
+ } from "./interfaces";
2
6
 
3
7
  export function deduplicateItems(items: FeedItem[]): FeedItem[] {
4
8
  const seen: Record<string, boolean> = {};
@@ -22,6 +26,11 @@ export function sortItems(items: FeedItem[]) {
22
26
  });
23
27
  }
24
28
 
29
+ type DateRangeParams = Pick<
30
+ FetchFeedOptionsForRequest,
31
+ "inserted_at.gte" | "inserted_at.lte" | "inserted_at.gt" | "inserted_at.lt"
32
+ >;
33
+
25
34
  export function mergeDateRangeParams(options: FeedClientOptions) {
26
35
  const { inserted_at_date_range, ...rest } = options;
27
36
 
@@ -29,7 +38,7 @@ export function mergeDateRangeParams(options: FeedClientOptions) {
29
38
  return rest;
30
39
  }
31
40
 
32
- const dateRangeParams: Record<string, string> = {};
41
+ const dateRangeParams: DateRangeParams = {};
33
42
 
34
43
  // Determine which operators to use based on the inclusive flag
35
44
  const isInclusive = inserted_at_date_range.inclusive ?? false;
@@ -157,34 +157,16 @@ const select = (state: StoreState, filters: SelectFilterParams = {}) => {
157
157
  const defaultGroup = findDefaultGroup(state.guideGroups);
158
158
  if (!defaultGroup) return result;
159
159
 
160
- const displaySequence = [...defaultGroup.display_sequence];
160
+ const displaySequence = defaultGroup.display_sequence;
161
161
  const location = state.location;
162
162
 
163
- // If in debug mode, put the forced guide at the beginning of the display sequence.
164
- if (state.debug.forcedGuideKey) {
165
- const forcedKeyIndex = displaySequence.indexOf(state.debug.forcedGuideKey);
166
- if (forcedKeyIndex > -1) {
167
- displaySequence.splice(forcedKeyIndex, 1);
168
- }
169
- displaySequence.unshift(state.debug.forcedGuideKey);
170
- }
171
-
172
163
  for (const [index, guideKey] of displaySequence.entries()) {
173
- let guide = state.guides[guideKey];
174
-
175
- // Use preview guide if it exists and matches the forced guide key
176
- if (
177
- state.debug.forcedGuideKey === guideKey &&
178
- state.previewGuides[guideKey]
179
- ) {
180
- guide = state.previewGuides[guideKey];
181
- }
182
-
164
+ const guide = state.previewGuides[guideKey] || state.guides[guideKey];
183
165
  if (!guide) continue;
184
166
 
185
- const affirmed = predicate(guide, {
167
+ const affirmed = predicate(guide, filters, {
186
168
  location,
187
- filters,
169
+ ineligibleGuides: state.ineligibleGuides,
188
170
  debug: state.debug,
189
171
  });
190
172
 
@@ -197,15 +179,15 @@ const select = (state: StoreState, filters: SelectFilterParams = {}) => {
197
179
  return result;
198
180
  };
199
181
 
200
- type PredicateOpts = {
201
- location?: string | undefined;
202
- filters?: SelectFilterParams | undefined;
203
- debug: DebugState;
204
- };
182
+ type PredicateOpts = Pick<
183
+ StoreState,
184
+ "location" | "ineligibleGuides" | "debug"
185
+ >;
205
186
 
206
187
  const predicate = (
207
188
  guide: KnockGuide,
208
- { location, filters = {}, debug = {} }: PredicateOpts,
189
+ filters: SelectFilterParams,
190
+ { location, ineligibleGuides = {}, debug = {} }: PredicateOpts,
209
191
  ) => {
210
192
  if (filters.type && filters.type !== guide.type) {
211
193
  return false;
@@ -215,11 +197,16 @@ const predicate = (
215
197
  return false;
216
198
  }
217
199
 
218
- // Bypass filtering if the debugged guide matches the given filters.
219
- // This should always run AFTER checking the filters but BEFORE
220
- // checking archived status and location rules.
221
- if (debug.forcedGuideKey === guide.key) {
222
- return true;
200
+ // If in debug mode with a forced guide key, bypass other filtering and always
201
+ // return true for that guide only. This should always run AFTER checking the
202
+ // filters but BEFORE checking archived status and location rules.
203
+ if (debug.forcedGuideKey) {
204
+ return debug.forcedGuideKey === guide.key;
205
+ }
206
+
207
+ const ineligible = ineligibleGuides[guide.key];
208
+ if (ineligible) {
209
+ return false;
223
210
  }
224
211
 
225
212
  if (!guide.active) {
@@ -230,6 +217,13 @@ const predicate = (
230
217
  return false;
231
218
  }
232
219
 
220
+ return checkActivatable(guide, location);
221
+ };
222
+
223
+ export const checkActivatable = (
224
+ guide: KnockGuide,
225
+ location: string | undefined,
226
+ ) => {
233
227
  const url = location ? newUrl(location) : undefined;
234
228
 
235
229
  const urlRules = guide.activation_url_rules || [];
@@ -283,18 +277,21 @@ export class KnockGuideClient {
283
277
  ) {
284
278
  const {
285
279
  trackLocationFromWindow = true,
280
+ // TODO(KNO-11523): Remove once we ship guide toolbar v2, and offload as
281
+ // much debugging specific logic and responsibilities to toolbar.
282
+ trackDebugParams = false,
286
283
  throttleCheckInterval = DEFAULT_COUNTER_INCREMENT_INTERVAL,
287
284
  } = options;
288
285
  const win = checkForWindow();
289
286
 
290
287
  const location = trackLocationFromWindow ? win?.location?.href : undefined;
291
-
292
- const debug = detectDebugParams();
288
+ const debug = trackDebugParams ? detectDebugParams() : undefined;
293
289
 
294
290
  this.store = new Store<StoreState>({
295
291
  guideGroups: [],
296
292
  guideGroupDisplayLogs: {},
297
293
  guides: {},
294
+ ineligibleGuides: {},
298
295
  previewGuides: {},
299
296
  queries: {},
300
297
  location,
@@ -376,7 +373,12 @@ export class KnockGuideClient {
376
373
  >(this.channelId, queryParams);
377
374
  queryStatus = { status: "ok" };
378
375
 
379
- const { entries, guide_groups: groups, guide_group_display_logs } = data;
376
+ const {
377
+ entries,
378
+ guide_groups: groups,
379
+ guide_group_display_logs,
380
+ ineligible_guides,
381
+ } = data;
380
382
 
381
383
  this.knock.log("[Guide] Loading fetched guides");
382
384
  this.store.setState((state) => ({
@@ -384,6 +386,7 @@ export class KnockGuideClient {
384
386
  guideGroups: groups?.length > 0 ? groups : [mockDefaultGroup(entries)],
385
387
  guideGroupDisplayLogs: guide_group_display_logs || {},
386
388
  guides: byKey(entries.map((g) => this.localCopy(g))),
389
+ ineligibleGuides: byKey(ineligible_guides || []),
387
390
  queries: { ...state.queries, [queryKey]: queryStatus },
388
391
  }));
389
392
  } catch (e) {
@@ -418,8 +421,9 @@ export class KnockGuideClient {
418
421
  const params = {
419
422
  ...this.targetParams,
420
423
  user_id: this.knock.userId,
421
- force_all_guides: debugState.forcedGuideKey ? true : undefined,
422
- preview_session_id: debugState.previewSessionId || undefined,
424
+ force_all_guides:
425
+ debugState?.forcedGuideKey || debugState?.debugging ? true : undefined,
426
+ preview_session_id: debugState?.previewSessionId || undefined,
423
427
  };
424
428
 
425
429
  const newChannel = this.socket.channel(this.socketChannelTopic, params);
@@ -484,6 +488,8 @@ export class KnockGuideClient {
484
488
  private handleSocketEvent(payload: GuideSocketEvent) {
485
489
  const { event, data } = payload;
486
490
 
491
+ // TODO(KNO-11489): Include an ineligible guide in the socket payload too
492
+ // and process it when handling socket events in real time.
487
493
  switch (event) {
488
494
  case "guide.added":
489
495
  return this.addOrReplaceGuide(payload);
@@ -565,6 +571,39 @@ export class KnockGuideClient {
565
571
  }
566
572
  }
567
573
 
574
+ setDebug(debugOpts?: Omit<DebugState, "debugging">) {
575
+ this.knock.log("[Guide] .setDebug()");
576
+ const shouldRefetch = !this.store.state.debug?.debugging;
577
+
578
+ this.store.setState((state) => ({
579
+ ...state,
580
+ debug: { ...debugOpts, debugging: true },
581
+ }));
582
+
583
+ if (shouldRefetch) {
584
+ this.knock.log(
585
+ `[Guide] Start debugging, refetching guides and resubscribing to the websocket channel`,
586
+ );
587
+ this.fetch();
588
+ this.subscribe();
589
+ }
590
+ }
591
+
592
+ unsetDebug() {
593
+ this.knock.log("[Guide] .unsetDebug()");
594
+ const shouldRefetch = this.store.state.debug?.debugging;
595
+
596
+ this.store.setState((state) => ({ ...state, debug: undefined }));
597
+
598
+ if (shouldRefetch) {
599
+ this.knock.log(
600
+ `[Guide] Stop debugging, refetching guides and resubscribing to the websocket channel`,
601
+ );
602
+ this.fetch();
603
+ this.subscribe();
604
+ }
605
+ }
606
+
568
607
  //
569
608
  // Store selector
570
609
  //
@@ -736,17 +775,8 @@ export class KnockGuideClient {
736
775
  // callback to a setTimeout, but just to be safe.
737
776
  this.ensureClearTimeout();
738
777
 
739
- // If in debug mode, try to resolve the forced guide, otherwise return the first non-undefined guide.
740
- let resolved = undefined;
741
- if (this.store.state.debug.forcedGuideKey) {
742
- resolved = this.stage.ordered.find(
743
- (x) => x === this.store.state.debug.forcedGuideKey,
744
- );
745
- }
746
-
747
- if (!resolved) {
748
- resolved = this.stage.ordered.find((x) => x !== undefined);
749
- }
778
+ // Resolve to the first non-undefined guide in the stage.
779
+ const resolved = this.stage.ordered.find((x) => x !== undefined);
750
780
 
751
781
  this.knock.log(
752
782
  `[Guide] Closing the current group stage: resolved=${resolved}`,
@@ -937,7 +967,7 @@ export class KnockGuideClient {
937
967
  // Get the next unarchived step.
938
968
  getStep() {
939
969
  // If debugging this guide, return the first step regardless of archive status
940
- if (self.store.state.debug.forcedGuideKey === this.key) {
970
+ if (self.store.state.debug?.forcedGuideKey === this.key) {
941
971
  return this.steps[0];
942
972
  }
943
973
 
@@ -994,7 +1024,7 @@ export class KnockGuideClient {
994
1024
 
995
1025
  // Append debug params
996
1026
  const debugState = this.store.state.debug;
997
- if (debugState.forcedGuideKey) {
1027
+ if (debugState?.forcedGuideKey || debugState?.debugging) {
998
1028
  combinedParams.force_all_guides = true;
999
1029
  }
1000
1030
 
@@ -1163,8 +1193,15 @@ export class KnockGuideClient {
1163
1193
 
1164
1194
  this.knock.log(`[Guide] Detected a location change: ${href}`);
1165
1195
 
1196
+ if (!this.options.trackDebugParams) {
1197
+ this.setLocation(href);
1198
+ return;
1199
+ }
1200
+
1201
+ // TODO(KNO-11523): Remove below once we ship toolbar v2.
1202
+
1166
1203
  // If entering debug mode, fetch all guides.
1167
- const currentDebugParams = this.store.state.debug;
1204
+ const currentDebugParams = this.store.state.debug || {};
1168
1205
  const newDebugParams = detectDebugParams();
1169
1206
 
1170
1207
  this.setLocation(href, { debug: newDebugParams });
@@ -1,9 +1,15 @@
1
- export { KnockGuideClient, DEBUG_QUERY_PARAMS } from "./client";
1
+ export {
2
+ KnockGuideClient,
3
+ DEBUG_QUERY_PARAMS,
4
+ checkActivatable,
5
+ } from "./client";
2
6
  export type {
3
7
  KnockGuide,
4
8
  KnockGuideStep,
9
+ GuideIneligibilityMarker as KnockGuideIneligibilityMarker,
5
10
  TargetParams as KnockGuideTargetParams,
6
11
  SelectFilterParams as KnockGuideFilterParams,
7
12
  SelectGuideOpts as KnockSelectGuideOpts,
8
13
  SelectGuidesOpts as KnockSelectGuidesOpts,
14
+ StoreState as KnockGuideClientStoreState,
9
15
  } from "./types";
@@ -65,6 +65,19 @@ export interface GuideGroupData {
65
65
  updated_at: string;
66
66
  }
67
67
 
68
+ type GuideIneligibilityReason =
69
+ | "guide_not_active"
70
+ | "marked_as_archived"
71
+ | "target_conditions_not_met"
72
+ | "not_in_target_audience";
73
+
74
+ export type GuideIneligibilityMarker = {
75
+ __typename: "GuideIneligibilityMarker";
76
+ key: KnockGuide["key"];
77
+ reason: GuideIneligibilityReason;
78
+ message: string;
79
+ };
80
+
68
81
  export type GetGuidesQueryParams = {
69
82
  data?: string;
70
83
  tenant?: string;
@@ -76,6 +89,7 @@ export type GetGuidesResponse = {
76
89
  entries: GuideData[];
77
90
  guide_groups: GuideGroupData[];
78
91
  guide_group_display_logs: Record<GuideGroupData["key"], string>;
92
+ ineligible_guides: GuideIneligibilityMarker[];
79
93
  };
80
94
 
81
95
  //
@@ -194,6 +208,7 @@ export type QueryStatus = {
194
208
  };
195
209
 
196
210
  export type DebugState = {
211
+ debugging?: boolean;
197
212
  forcedGuideKey?: string | null;
198
213
  previewSessionId?: string | null;
199
214
  };
@@ -202,11 +217,15 @@ export type StoreState = {
202
217
  guideGroups: GuideGroupData[];
203
218
  guideGroupDisplayLogs: Record<GuideGroupData["key"], string>;
204
219
  guides: Record<KnockGuide["key"], KnockGuide>;
220
+ ineligibleGuides: Record<
221
+ GuideIneligibilityMarker["key"],
222
+ GuideIneligibilityMarker
223
+ >;
205
224
  previewGuides: Record<KnockGuide["key"], KnockGuide>;
206
225
  queries: Record<QueryKey, QueryStatus>;
207
226
  location: string | undefined;
208
227
  counter: number;
209
- debug: DebugState;
228
+ debug?: DebugState;
210
229
  };
211
230
 
212
231
  export type QueryFilterParams = Pick<GetGuidesQueryParams, "type">;
@@ -229,6 +248,7 @@ export type TargetParams = {
229
248
 
230
249
  export type ConstructorOpts = {
231
250
  trackLocationFromWindow?: boolean;
251
+ trackDebugParams?: boolean;
232
252
  orderResolutionDuration?: number;
233
253
  throttleCheckInterval?: number;
234
254
  };