@knocklabs/client 0.21.0 → 0.21.2
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/CHANGELOG.md +14 -0
- package/dist/cjs/api.js +1 -1
- package/dist/cjs/api.js.map +1 -1
- package/dist/cjs/clients/feed/feed.js +1 -1
- package/dist/cjs/clients/feed/feed.js.map +1 -1
- package/dist/cjs/clients/feed/utils.js.map +1 -1
- package/dist/cjs/clients/guide/client.js +1 -1
- package/dist/cjs/clients/guide/client.js.map +1 -1
- package/dist/cjs/clients/guide/helpers.js +1 -1
- package/dist/cjs/clients/guide/helpers.js.map +1 -1
- package/dist/cjs/clients/guide/types.js +2 -0
- package/dist/cjs/clients/guide/types.js.map +1 -0
- package/dist/cjs/helpers.js +1 -1
- package/dist/cjs/helpers.js.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/knock.js +2 -2
- package/dist/cjs/knock.js.map +1 -1
- package/dist/cjs/pageVisibility.js +2 -0
- package/dist/cjs/pageVisibility.js.map +1 -0
- package/dist/esm/api.mjs +27 -12
- package/dist/esm/api.mjs.map +1 -1
- package/dist/esm/clients/feed/feed.mjs +60 -87
- package/dist/esm/clients/feed/feed.mjs.map +1 -1
- package/dist/esm/clients/feed/utils.mjs.map +1 -1
- package/dist/esm/clients/guide/client.mjs +270 -204
- package/dist/esm/clients/guide/client.mjs.map +1 -1
- package/dist/esm/clients/guide/helpers.mjs +34 -44
- package/dist/esm/clients/guide/helpers.mjs.map +1 -1
- package/dist/esm/clients/guide/types.mjs +13 -0
- package/dist/esm/clients/guide/types.mjs.map +1 -0
- package/dist/esm/helpers.mjs +19 -4
- package/dist/esm/helpers.mjs.map +1 -1
- package/dist/esm/index.mjs +13 -10
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/knock.mjs +31 -29
- package/dist/esm/knock.mjs.map +1 -1
- package/dist/esm/pageVisibility.mjs +31 -0
- package/dist/esm/pageVisibility.mjs.map +1 -0
- package/dist/types/api.d.ts +4 -0
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/clients/feed/feed.d.ts +1 -11
- package/dist/types/clients/feed/feed.d.ts.map +1 -1
- package/dist/types/clients/feed/interfaces.d.ts +15 -5
- package/dist/types/clients/feed/interfaces.d.ts.map +1 -1
- package/dist/types/clients/feed/utils.d.ts +0 -2
- package/dist/types/clients/feed/utils.d.ts.map +1 -1
- package/dist/types/clients/guide/client.d.ts +6 -1
- package/dist/types/clients/guide/client.d.ts.map +1 -1
- package/dist/types/clients/guide/helpers.d.ts +1 -7
- package/dist/types/clients/guide/helpers.d.ts.map +1 -1
- package/dist/types/clients/guide/index.d.ts +3 -2
- package/dist/types/clients/guide/index.d.ts.map +1 -1
- package/dist/types/clients/guide/types.d.ts +33 -1
- package/dist/types/clients/guide/types.d.ts.map +1 -1
- package/dist/types/helpers.d.ts +19 -0
- package/dist/types/helpers.d.ts.map +1 -1
- package/dist/types/interfaces.d.ts +2 -0
- package/dist/types/interfaces.d.ts.map +1 -1
- package/dist/types/knock.d.ts +1 -0
- package/dist/types/knock.d.ts.map +1 -1
- package/dist/types/pageVisibility.d.ts +22 -0
- package/dist/types/pageVisibility.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/api.ts +30 -0
- package/src/clients/feed/feed.ts +0 -73
- package/src/clients/feed/interfaces.ts +15 -11
- package/src/clients/feed/utils.ts +11 -2
- package/src/clients/guide/client.ts +206 -84
- package/src/clients/guide/helpers.ts +0 -12
- package/src/clients/guide/index.ts +10 -1
- package/src/clients/guide/types.ts +55 -1
- package/src/helpers.ts +39 -0
- package/src/interfaces.ts +2 -0
- package/src/knock.ts +4 -3
- package/src/pageVisibility.ts +70 -0
|
@@ -7,7 +7,6 @@ import Knock from "../../knock";
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_GROUP_KEY,
|
|
10
|
-
SelectionResult,
|
|
11
10
|
byKey,
|
|
12
11
|
checkStateIfThrottled,
|
|
13
12
|
findDefaultGroup,
|
|
@@ -46,6 +45,8 @@ import {
|
|
|
46
45
|
SelectFilterParams,
|
|
47
46
|
SelectGuideOpts,
|
|
48
47
|
SelectGuidesOpts,
|
|
48
|
+
SelectQueryLimit,
|
|
49
|
+
SelectionResult,
|
|
49
50
|
StepMessageState,
|
|
50
51
|
StoreState,
|
|
51
52
|
TargetParams,
|
|
@@ -150,41 +151,32 @@ const safeJsonParseDebugParams = (value: string): DebugState => {
|
|
|
150
151
|
}
|
|
151
152
|
};
|
|
152
153
|
|
|
153
|
-
|
|
154
|
+
type SelectQueryMetadata = {
|
|
155
|
+
limit: SelectQueryLimit;
|
|
156
|
+
opts: SelectGuideOpts;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const select = (
|
|
160
|
+
state: StoreState,
|
|
161
|
+
filters: SelectFilterParams,
|
|
162
|
+
metadata: SelectQueryMetadata,
|
|
163
|
+
) => {
|
|
154
164
|
// A map of selected guides as values, with its order index as keys.
|
|
155
165
|
const result = new SelectionResult();
|
|
156
166
|
|
|
157
167
|
const defaultGroup = findDefaultGroup(state.guideGroups);
|
|
158
168
|
if (!defaultGroup) return result;
|
|
159
169
|
|
|
160
|
-
const displaySequence =
|
|
170
|
+
const displaySequence = defaultGroup.display_sequence;
|
|
161
171
|
const location = state.location;
|
|
162
172
|
|
|
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
173
|
for (const [index, guideKey] of displaySequence.entries()) {
|
|
173
|
-
|
|
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
|
-
|
|
174
|
+
const guide = state.previewGuides[guideKey] || state.guides[guideKey];
|
|
183
175
|
if (!guide) continue;
|
|
184
176
|
|
|
185
|
-
const affirmed = predicate(guide, {
|
|
177
|
+
const affirmed = predicate(guide, filters, {
|
|
186
178
|
location,
|
|
187
|
-
|
|
179
|
+
ineligibleGuides: state.ineligibleGuides,
|
|
188
180
|
debug: state.debug,
|
|
189
181
|
});
|
|
190
182
|
|
|
@@ -193,19 +185,20 @@ const select = (state: StoreState, filters: SelectFilterParams = {}) => {
|
|
|
193
185
|
result.set(index, guide);
|
|
194
186
|
}
|
|
195
187
|
|
|
196
|
-
result.metadata = { guideGroup: defaultGroup };
|
|
188
|
+
result.metadata = { guideGroup: defaultGroup, filters, ...metadata };
|
|
189
|
+
|
|
197
190
|
return result;
|
|
198
191
|
};
|
|
199
192
|
|
|
200
|
-
type PredicateOpts =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
};
|
|
193
|
+
type PredicateOpts = Pick<
|
|
194
|
+
StoreState,
|
|
195
|
+
"location" | "ineligibleGuides" | "debug"
|
|
196
|
+
>;
|
|
205
197
|
|
|
206
198
|
const predicate = (
|
|
207
199
|
guide: KnockGuide,
|
|
208
|
-
|
|
200
|
+
filters: SelectFilterParams,
|
|
201
|
+
{ location, ineligibleGuides = {}, debug = {} }: PredicateOpts,
|
|
209
202
|
) => {
|
|
210
203
|
if (filters.type && filters.type !== guide.type) {
|
|
211
204
|
return false;
|
|
@@ -215,11 +208,16 @@ const predicate = (
|
|
|
215
208
|
return false;
|
|
216
209
|
}
|
|
217
210
|
|
|
218
|
-
//
|
|
219
|
-
// This should always run AFTER checking the
|
|
220
|
-
// checking archived status and location rules.
|
|
221
|
-
if (debug.forcedGuideKey
|
|
222
|
-
return
|
|
211
|
+
// If in debug mode with a forced guide key, bypass other filtering and always
|
|
212
|
+
// return true for that guide only. This should always run AFTER checking the
|
|
213
|
+
// filters but BEFORE checking archived status and location rules.
|
|
214
|
+
if (debug.forcedGuideKey) {
|
|
215
|
+
return debug.forcedGuideKey === guide.key;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const ineligible = ineligibleGuides[guide.key];
|
|
219
|
+
if (ineligible) {
|
|
220
|
+
return false;
|
|
223
221
|
}
|
|
224
222
|
|
|
225
223
|
if (!guide.active) {
|
|
@@ -230,6 +228,13 @@ const predicate = (
|
|
|
230
228
|
return false;
|
|
231
229
|
}
|
|
232
230
|
|
|
231
|
+
return checkActivatable(guide, location);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export const checkActivatable = (
|
|
235
|
+
guide: KnockGuide,
|
|
236
|
+
location: string | undefined,
|
|
237
|
+
) => {
|
|
233
238
|
const url = location ? newUrl(location) : undefined;
|
|
234
239
|
|
|
235
240
|
const urlRules = guide.activation_url_rules || [];
|
|
@@ -283,18 +288,21 @@ export class KnockGuideClient {
|
|
|
283
288
|
) {
|
|
284
289
|
const {
|
|
285
290
|
trackLocationFromWindow = true,
|
|
291
|
+
// TODO(KNO-11523): Remove once we ship guide toolbar v2, and offload as
|
|
292
|
+
// much debugging specific logic and responsibilities to toolbar.
|
|
293
|
+
trackDebugParams = false,
|
|
286
294
|
throttleCheckInterval = DEFAULT_COUNTER_INCREMENT_INTERVAL,
|
|
287
295
|
} = options;
|
|
288
296
|
const win = checkForWindow();
|
|
289
297
|
|
|
290
298
|
const location = trackLocationFromWindow ? win?.location?.href : undefined;
|
|
291
|
-
|
|
292
|
-
const debug = detectDebugParams();
|
|
299
|
+
const debug = trackDebugParams ? detectDebugParams() : undefined;
|
|
293
300
|
|
|
294
301
|
this.store = new Store<StoreState>({
|
|
295
302
|
guideGroups: [],
|
|
296
303
|
guideGroupDisplayLogs: {},
|
|
297
304
|
guides: {},
|
|
305
|
+
ineligibleGuides: {},
|
|
298
306
|
previewGuides: {},
|
|
299
307
|
queries: {},
|
|
300
308
|
location,
|
|
@@ -376,7 +384,12 @@ export class KnockGuideClient {
|
|
|
376
384
|
>(this.channelId, queryParams);
|
|
377
385
|
queryStatus = { status: "ok" };
|
|
378
386
|
|
|
379
|
-
const {
|
|
387
|
+
const {
|
|
388
|
+
entries,
|
|
389
|
+
guide_groups: groups,
|
|
390
|
+
guide_group_display_logs,
|
|
391
|
+
ineligible_guides,
|
|
392
|
+
} = data;
|
|
380
393
|
|
|
381
394
|
this.knock.log("[Guide] Loading fetched guides");
|
|
382
395
|
this.store.setState((state) => ({
|
|
@@ -384,6 +397,7 @@ export class KnockGuideClient {
|
|
|
384
397
|
guideGroups: groups?.length > 0 ? groups : [mockDefaultGroup(entries)],
|
|
385
398
|
guideGroupDisplayLogs: guide_group_display_logs || {},
|
|
386
399
|
guides: byKey(entries.map((g) => this.localCopy(g))),
|
|
400
|
+
ineligibleGuides: byKey(ineligible_guides || []),
|
|
387
401
|
queries: { ...state.queries, [queryKey]: queryStatus },
|
|
388
402
|
}));
|
|
389
403
|
} catch (e) {
|
|
@@ -418,8 +432,9 @@ export class KnockGuideClient {
|
|
|
418
432
|
const params = {
|
|
419
433
|
...this.targetParams,
|
|
420
434
|
user_id: this.knock.userId,
|
|
421
|
-
force_all_guides:
|
|
422
|
-
|
|
435
|
+
force_all_guides:
|
|
436
|
+
debugState?.forcedGuideKey || debugState?.debugging ? true : undefined,
|
|
437
|
+
preview_session_id: debugState?.previewSessionId || undefined,
|
|
423
438
|
};
|
|
424
439
|
|
|
425
440
|
const newChannel = this.socket.channel(this.socketChannelTopic, params);
|
|
@@ -484,6 +499,8 @@ export class KnockGuideClient {
|
|
|
484
499
|
private handleSocketEvent(payload: GuideSocketEvent) {
|
|
485
500
|
const { event, data } = payload;
|
|
486
501
|
|
|
502
|
+
// TODO(KNO-11489): Include an ineligible guide in the socket payload too
|
|
503
|
+
// and process it when handling socket events in real time.
|
|
487
504
|
switch (event) {
|
|
488
505
|
case "guide.added":
|
|
489
506
|
return this.addOrReplaceGuide(payload);
|
|
@@ -565,6 +582,39 @@ export class KnockGuideClient {
|
|
|
565
582
|
}
|
|
566
583
|
}
|
|
567
584
|
|
|
585
|
+
setDebug(debugOpts?: Omit<DebugState, "debugging">) {
|
|
586
|
+
this.knock.log("[Guide] .setDebug()");
|
|
587
|
+
const shouldRefetch = !this.store.state.debug?.debugging;
|
|
588
|
+
|
|
589
|
+
this.store.setState((state) => ({
|
|
590
|
+
...state,
|
|
591
|
+
debug: { ...debugOpts, debugging: true },
|
|
592
|
+
}));
|
|
593
|
+
|
|
594
|
+
if (shouldRefetch) {
|
|
595
|
+
this.knock.log(
|
|
596
|
+
`[Guide] Start debugging, refetching guides and resubscribing to the websocket channel`,
|
|
597
|
+
);
|
|
598
|
+
this.fetch();
|
|
599
|
+
this.subscribe();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
unsetDebug() {
|
|
604
|
+
this.knock.log("[Guide] .unsetDebug()");
|
|
605
|
+
const shouldRefetch = this.store.state.debug?.debugging;
|
|
606
|
+
|
|
607
|
+
this.store.setState((state) => ({ ...state, debug: undefined }));
|
|
608
|
+
|
|
609
|
+
if (shouldRefetch) {
|
|
610
|
+
this.knock.log(
|
|
611
|
+
`[Guide] Stop debugging, refetching guides and resubscribing to the websocket channel`,
|
|
612
|
+
);
|
|
613
|
+
this.fetch();
|
|
614
|
+
this.subscribe();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
568
618
|
//
|
|
569
619
|
// Store selector
|
|
570
620
|
//
|
|
@@ -578,14 +628,35 @@ export class KnockGuideClient {
|
|
|
578
628
|
`[Guide] .selectGuides (filters: ${formatFilters(filters)}; state: ${formatState(state)})`,
|
|
579
629
|
);
|
|
580
630
|
|
|
581
|
-
|
|
582
|
-
|
|
631
|
+
// 1. First, call selectGuide() using the same filters to ensure we have a
|
|
632
|
+
// group stage open and respect throttling. This isn't the real query, but
|
|
633
|
+
// rather it's a shortcut ahead of handling the actual query result below.
|
|
634
|
+
const selectedGuide = this.selectGuide(state, filters, {
|
|
635
|
+
...opts,
|
|
636
|
+
// Don't record this result, not the actual query result we need.
|
|
637
|
+
recordSelectQuery: false,
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// 2. Now make the actual select query with the provided filters and opts,
|
|
641
|
+
// and record the result (as needed). By default, we only record the result
|
|
642
|
+
// while in debugging.
|
|
643
|
+
const { recordSelectQuery = !!state.debug?.debugging } = opts;
|
|
644
|
+
const metadata: SelectQueryMetadata = {
|
|
645
|
+
limit: "all",
|
|
646
|
+
opts: { ...opts, recordSelectQuery },
|
|
647
|
+
};
|
|
648
|
+
const result = select(state, filters, metadata);
|
|
649
|
+
this.maybeRecordSelectResult(result);
|
|
650
|
+
|
|
651
|
+
// 3. Stop if there is not at least one guide to return.
|
|
652
|
+
if (!selectedGuide && !opts.includeThrottled) {
|
|
583
653
|
return [];
|
|
584
654
|
}
|
|
585
655
|
|
|
586
656
|
// There should be at least one guide to return here now.
|
|
587
|
-
const guides = [...
|
|
657
|
+
const guides = [...result.values()];
|
|
588
658
|
|
|
659
|
+
// 4. If throttled, filter out any throttled guides.
|
|
589
660
|
if (!opts.includeThrottled && checkStateIfThrottled(state)) {
|
|
590
661
|
const unthrottledGuides = guides.filter(
|
|
591
662
|
(g) => g.bypass_global_group_limit,
|
|
@@ -618,32 +689,6 @@ export class KnockGuideClient {
|
|
|
618
689
|
return undefined;
|
|
619
690
|
}
|
|
620
691
|
|
|
621
|
-
const result = select(state, filters);
|
|
622
|
-
|
|
623
|
-
if (result.size === 0) {
|
|
624
|
-
this.knock.log("[Guide] Selection found zero result");
|
|
625
|
-
return undefined;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const [index, guide] = [...result][0]!;
|
|
629
|
-
this.knock.log(
|
|
630
|
-
`[Guide] Selection found: \`${guide.key}\` (total: ${result.size})`,
|
|
631
|
-
);
|
|
632
|
-
|
|
633
|
-
// If a guide ignores the group limit, then return immediately to render
|
|
634
|
-
// always.
|
|
635
|
-
if (guide.bypass_global_group_limit) {
|
|
636
|
-
this.knock.log(`[Guide] Returning the unthrottled guide: ${guide.key}`);
|
|
637
|
-
return guide;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Check if inside the throttle window (i.e. throttled) and if so stop and
|
|
641
|
-
// return undefined unless explicitly given the option to include throttled.
|
|
642
|
-
if (!opts.includeThrottled && checkStateIfThrottled(state)) {
|
|
643
|
-
this.knock.log(`[Guide] Throttling the selected guide: ${guide.key}`);
|
|
644
|
-
return undefined;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
692
|
// Starting here to the end of this method represents the core logic of how
|
|
648
693
|
// "group stage" works. It provides a mechanism for 1) figuring out which
|
|
649
694
|
// guide components are about to render on a page, 2) determining which
|
|
@@ -677,6 +722,35 @@ export class KnockGuideClient {
|
|
|
677
722
|
this.stage = this.openGroupStage(); // Assign here to make tsc happy
|
|
678
723
|
}
|
|
679
724
|
|
|
725
|
+
// Must come AFTER we ensure a group stage exists above, so we can record
|
|
726
|
+
// select queries. By default, we only record the result while in debugging.
|
|
727
|
+
const { recordSelectQuery = !!state.debug?.debugging } = opts;
|
|
728
|
+
const metadata: SelectQueryMetadata = {
|
|
729
|
+
limit: "one",
|
|
730
|
+
opts: { ...opts, recordSelectQuery },
|
|
731
|
+
};
|
|
732
|
+
const result = select(state, filters, metadata);
|
|
733
|
+
this.maybeRecordSelectResult(result);
|
|
734
|
+
|
|
735
|
+
if (result.size === 0) {
|
|
736
|
+
this.knock.log("[Guide] Selection found zero result");
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const [index, guide] = [...result][0]!;
|
|
741
|
+
this.knock.log(
|
|
742
|
+
`[Guide] Selection found: \`${guide.key}\` (total: ${result.size})`,
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
// If a guide ignores the group limit, then return immediately to render
|
|
746
|
+
// always.
|
|
747
|
+
if (guide.bypass_global_group_limit) {
|
|
748
|
+
this.knock.log(`[Guide] Returning the unthrottled guide: ${guide.key}`);
|
|
749
|
+
return guide;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const throttled = !opts.includeThrottled && checkStateIfThrottled(state);
|
|
753
|
+
|
|
680
754
|
switch (this.stage.status) {
|
|
681
755
|
case "open": {
|
|
682
756
|
this.knock.log(`[Guide] Adding to the group stage: ${guide.key}`);
|
|
@@ -686,8 +760,16 @@ export class KnockGuideClient {
|
|
|
686
760
|
|
|
687
761
|
case "patch": {
|
|
688
762
|
this.knock.log(`[Guide] Patching the group stage: ${guide.key}`);
|
|
763
|
+
// Refresh the ordered queue in the group stage while continuing to
|
|
764
|
+
// render the currently resolved guide while in patch window, so that
|
|
765
|
+
// we can re-resolve when the group stage closes.
|
|
689
766
|
this.stage.ordered[index] = guide.key;
|
|
690
767
|
|
|
768
|
+
if (throttled) {
|
|
769
|
+
this.knock.log(`[Guide] Throttling the selected guide: ${guide.key}`);
|
|
770
|
+
return undefined;
|
|
771
|
+
}
|
|
772
|
+
|
|
691
773
|
const ret = this.stage.resolved === guide.key ? guide : undefined;
|
|
692
774
|
this.knock.log(
|
|
693
775
|
`[Guide] Returning \`${ret?.key}\` (stage: ${formatGroupStage(this.stage)})`,
|
|
@@ -696,6 +778,11 @@ export class KnockGuideClient {
|
|
|
696
778
|
}
|
|
697
779
|
|
|
698
780
|
case "closed": {
|
|
781
|
+
if (throttled) {
|
|
782
|
+
this.knock.log(`[Guide] Throttling the selected guide: ${guide.key}`);
|
|
783
|
+
return undefined;
|
|
784
|
+
}
|
|
785
|
+
|
|
699
786
|
const ret = this.stage.resolved === guide.key ? guide : undefined;
|
|
700
787
|
this.knock.log(
|
|
701
788
|
`[Guide] Returning \`${ret?.key}\` (stage: ${formatGroupStage(this.stage)})`,
|
|
@@ -705,6 +792,42 @@ export class KnockGuideClient {
|
|
|
705
792
|
}
|
|
706
793
|
}
|
|
707
794
|
|
|
795
|
+
// Record select query results by accumulating them by 1) key or type first,
|
|
796
|
+
// and then 2) "one" or "all".
|
|
797
|
+
private maybeRecordSelectResult(result: SelectionResult) {
|
|
798
|
+
if (!result.metadata) return;
|
|
799
|
+
|
|
800
|
+
const { opts, filters, limit } = result.metadata;
|
|
801
|
+
if (!opts.recordSelectQuery) return;
|
|
802
|
+
if (!filters.key && !filters.type) return;
|
|
803
|
+
if (!this.stage || this.stage.status === "closed") return;
|
|
804
|
+
|
|
805
|
+
// Deep merge to accumulate the results.
|
|
806
|
+
const queriedByKey = this.stage.results.key || {};
|
|
807
|
+
if (filters.key) {
|
|
808
|
+
queriedByKey[filters.key] = {
|
|
809
|
+
...(queriedByKey[filters.key] || {}),
|
|
810
|
+
...{ [limit]: result },
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
const queriedByType = this.stage.results.type || {};
|
|
814
|
+
if (filters.type) {
|
|
815
|
+
queriedByType[filters.type] = {
|
|
816
|
+
...(queriedByType[filters.type] || {}),
|
|
817
|
+
...{ [limit]: result },
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
this.stage = {
|
|
822
|
+
...this.stage,
|
|
823
|
+
results: { key: queriedByKey, type: queriedByType },
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
getStage() {
|
|
828
|
+
return this.stage;
|
|
829
|
+
}
|
|
830
|
+
|
|
708
831
|
private openGroupStage() {
|
|
709
832
|
this.knock.log("[Guide] Opening a new group stage");
|
|
710
833
|
|
|
@@ -720,6 +843,7 @@ export class KnockGuideClient {
|
|
|
720
843
|
this.stage = {
|
|
721
844
|
status: "open",
|
|
722
845
|
ordered: [],
|
|
846
|
+
results: {},
|
|
723
847
|
timeoutId,
|
|
724
848
|
};
|
|
725
849
|
|
|
@@ -736,17 +860,8 @@ export class KnockGuideClient {
|
|
|
736
860
|
// callback to a setTimeout, but just to be safe.
|
|
737
861
|
this.ensureClearTimeout();
|
|
738
862
|
|
|
739
|
-
//
|
|
740
|
-
|
|
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
|
-
}
|
|
863
|
+
// Resolve to the first non-undefined guide in the stage.
|
|
864
|
+
const resolved = this.stage.ordered.find((x) => x !== undefined);
|
|
750
865
|
|
|
751
866
|
this.knock.log(
|
|
752
867
|
`[Guide] Closing the current group stage: resolved=${resolved}`,
|
|
@@ -937,7 +1052,7 @@ export class KnockGuideClient {
|
|
|
937
1052
|
// Get the next unarchived step.
|
|
938
1053
|
getStep() {
|
|
939
1054
|
// If debugging this guide, return the first step regardless of archive status
|
|
940
|
-
if (self.store.state.debug
|
|
1055
|
+
if (self.store.state.debug?.forcedGuideKey === this.key) {
|
|
941
1056
|
return this.steps[0];
|
|
942
1057
|
}
|
|
943
1058
|
|
|
@@ -994,7 +1109,7 @@ export class KnockGuideClient {
|
|
|
994
1109
|
|
|
995
1110
|
// Append debug params
|
|
996
1111
|
const debugState = this.store.state.debug;
|
|
997
|
-
if (debugState
|
|
1112
|
+
if (debugState?.forcedGuideKey || debugState?.debugging) {
|
|
998
1113
|
combinedParams.force_all_guides = true;
|
|
999
1114
|
}
|
|
1000
1115
|
|
|
@@ -1163,8 +1278,15 @@ export class KnockGuideClient {
|
|
|
1163
1278
|
|
|
1164
1279
|
this.knock.log(`[Guide] Detected a location change: ${href}`);
|
|
1165
1280
|
|
|
1281
|
+
if (!this.options.trackDebugParams) {
|
|
1282
|
+
this.setLocation(href);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// TODO(KNO-11523): Remove below once we ship toolbar v2.
|
|
1287
|
+
|
|
1166
1288
|
// If entering debug mode, fetch all guides.
|
|
1167
|
-
const currentDebugParams = this.store.state.debug;
|
|
1289
|
+
const currentDebugParams = this.store.state.debug || {};
|
|
1168
1290
|
const newDebugParams = detectDebugParams();
|
|
1169
1291
|
|
|
1170
1292
|
this.setLocation(href, { debug: newDebugParams });
|
|
@@ -3,23 +3,11 @@ import {
|
|
|
3
3
|
GuideActivationUrlRuleData,
|
|
4
4
|
GuideData,
|
|
5
5
|
GuideGroupData,
|
|
6
|
-
KnockGuide,
|
|
7
6
|
KnockGuideActivationUrlPattern,
|
|
8
7
|
SelectFilterParams,
|
|
9
8
|
StoreState,
|
|
10
9
|
} from "./types";
|
|
11
10
|
|
|
12
|
-
// Extends the map class to allow having metadata on it, which is used to record
|
|
13
|
-
// the guide group context for the selection result (though currently only a
|
|
14
|
-
// default global group is supported).
|
|
15
|
-
export class SelectionResult<K = number, V = KnockGuide> extends Map<K, V> {
|
|
16
|
-
metadata: { guideGroup: GuideGroupData } | undefined;
|
|
17
|
-
|
|
18
|
-
constructor() {
|
|
19
|
-
super();
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
11
|
export const formatGroupStage = (stage: GroupStage) => {
|
|
24
12
|
return `status=${stage.status}, resolved=${stage.resolved}`;
|
|
25
13
|
};
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
KnockGuideClient,
|
|
3
|
+
DEBUG_QUERY_PARAMS,
|
|
4
|
+
checkActivatable,
|
|
5
|
+
} from "./client";
|
|
6
|
+
export { checkStateIfThrottled } from "./helpers";
|
|
2
7
|
export type {
|
|
3
8
|
KnockGuide,
|
|
4
9
|
KnockGuideStep,
|
|
10
|
+
GuideIneligibilityMarker as KnockGuideIneligibilityMarker,
|
|
5
11
|
TargetParams as KnockGuideTargetParams,
|
|
6
12
|
SelectFilterParams as KnockGuideFilterParams,
|
|
7
13
|
SelectGuideOpts as KnockSelectGuideOpts,
|
|
8
14
|
SelectGuidesOpts as KnockSelectGuidesOpts,
|
|
15
|
+
StoreState as KnockGuideClientStoreState,
|
|
16
|
+
GroupStage as KnockGuideClientGroupStage,
|
|
17
|
+
SelectionResult as KnockGuideSelectionResult,
|
|
9
18
|
} from "./types";
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { GenericData } from "@knocklabs/types";
|
|
2
2
|
|
|
3
|
+
// i.e. useGuide vs useGuides
|
|
4
|
+
export type SelectQueryLimit = "one" | "all";
|
|
5
|
+
|
|
6
|
+
type SelectionResultMetadata = {
|
|
7
|
+
guideGroup: GuideGroupData;
|
|
8
|
+
// Additional info about the underlying select query behind the result.
|
|
9
|
+
filters: SelectFilterParams;
|
|
10
|
+
limit: SelectQueryLimit;
|
|
11
|
+
opts: SelectGuideOpts;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Extends the map class to allow having metadata on it, which is used to record
|
|
15
|
+
// the guide group context for the selection result (though currently only a
|
|
16
|
+
// default global group is supported).
|
|
17
|
+
export class SelectionResult<K = number, V = KnockGuide> extends Map<K, V> {
|
|
18
|
+
metadata: SelectionResultMetadata | undefined;
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
super();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
3
25
|
//
|
|
4
26
|
// Fetch guides API
|
|
5
27
|
//
|
|
@@ -65,6 +87,19 @@ export interface GuideGroupData {
|
|
|
65
87
|
updated_at: string;
|
|
66
88
|
}
|
|
67
89
|
|
|
90
|
+
type GuideIneligibilityReason =
|
|
91
|
+
| "guide_not_active"
|
|
92
|
+
| "marked_as_archived"
|
|
93
|
+
| "target_conditions_not_met"
|
|
94
|
+
| "not_in_target_audience";
|
|
95
|
+
|
|
96
|
+
export type GuideIneligibilityMarker = {
|
|
97
|
+
__typename: "GuideIneligibilityMarker";
|
|
98
|
+
key: KnockGuide["key"];
|
|
99
|
+
reason: GuideIneligibilityReason;
|
|
100
|
+
message: string;
|
|
101
|
+
};
|
|
102
|
+
|
|
68
103
|
export type GetGuidesQueryParams = {
|
|
69
104
|
data?: string;
|
|
70
105
|
tenant?: string;
|
|
@@ -76,6 +111,7 @@ export type GetGuidesResponse = {
|
|
|
76
111
|
entries: GuideData[];
|
|
77
112
|
guide_groups: GuideGroupData[];
|
|
78
113
|
guide_group_display_logs: Record<GuideGroupData["key"], string>;
|
|
114
|
+
ineligible_guides: GuideIneligibilityMarker[];
|
|
79
115
|
};
|
|
80
116
|
|
|
81
117
|
//
|
|
@@ -194,6 +230,7 @@ export type QueryStatus = {
|
|
|
194
230
|
};
|
|
195
231
|
|
|
196
232
|
export type DebugState = {
|
|
233
|
+
debugging?: boolean;
|
|
197
234
|
forcedGuideKey?: string | null;
|
|
198
235
|
previewSessionId?: string | null;
|
|
199
236
|
};
|
|
@@ -202,11 +239,15 @@ export type StoreState = {
|
|
|
202
239
|
guideGroups: GuideGroupData[];
|
|
203
240
|
guideGroupDisplayLogs: Record<GuideGroupData["key"], string>;
|
|
204
241
|
guides: Record<KnockGuide["key"], KnockGuide>;
|
|
242
|
+
ineligibleGuides: Record<
|
|
243
|
+
GuideIneligibilityMarker["key"],
|
|
244
|
+
GuideIneligibilityMarker
|
|
245
|
+
>;
|
|
205
246
|
previewGuides: Record<KnockGuide["key"], KnockGuide>;
|
|
206
247
|
queries: Record<QueryKey, QueryStatus>;
|
|
207
248
|
location: string | undefined;
|
|
208
249
|
counter: number;
|
|
209
|
-
debug
|
|
250
|
+
debug?: DebugState;
|
|
210
251
|
};
|
|
211
252
|
|
|
212
253
|
export type QueryFilterParams = Pick<GetGuidesQueryParams, "type">;
|
|
@@ -218,6 +259,7 @@ export type SelectFilterParams = {
|
|
|
218
259
|
|
|
219
260
|
export type SelectGuideOpts = {
|
|
220
261
|
includeThrottled?: boolean;
|
|
262
|
+
recordSelectQuery?: boolean;
|
|
221
263
|
};
|
|
222
264
|
|
|
223
265
|
export type SelectGuidesOpts = SelectGuideOpts;
|
|
@@ -229,13 +271,25 @@ export type TargetParams = {
|
|
|
229
271
|
|
|
230
272
|
export type ConstructorOpts = {
|
|
231
273
|
trackLocationFromWindow?: boolean;
|
|
274
|
+
trackDebugParams?: boolean;
|
|
232
275
|
orderResolutionDuration?: number;
|
|
233
276
|
throttleCheckInterval?: number;
|
|
234
277
|
};
|
|
235
278
|
|
|
279
|
+
type SelectionResultByLimit = {
|
|
280
|
+
one?: SelectionResult;
|
|
281
|
+
all?: SelectionResult;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
type RecordedSelectionResults = {
|
|
285
|
+
key?: Record<KnockGuide["key"], SelectionResultByLimit>;
|
|
286
|
+
type?: Record<KnockGuide["type"], SelectionResultByLimit>;
|
|
287
|
+
};
|
|
288
|
+
|
|
236
289
|
export type GroupStage = {
|
|
237
290
|
status: "open" | "closed" | "patch";
|
|
238
291
|
ordered: Array<KnockGuide["key"]>;
|
|
239
292
|
resolved?: KnockGuide["key"];
|
|
240
293
|
timeoutId: ReturnType<typeof setTimeout> | null;
|
|
294
|
+
results: RecordedSelectionResults;
|
|
241
295
|
};
|
package/src/helpers.ts
CHANGED
|
@@ -4,3 +4,42 @@ const uuidRegex =
|
|
|
4
4
|
export function isValidUuid(uuid: string) {
|
|
5
5
|
return uuidRegex.test(uuid);
|
|
6
6
|
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Exponential backoff with full jitter and a minimum delay floor.
|
|
10
|
+
*
|
|
11
|
+
* - Uses exponential growth capped at maxDelayMs
|
|
12
|
+
* - Applies full jitter to spread retries uniformly across the window
|
|
13
|
+
* - Enforces a minimum delay to avoid tight retry loops
|
|
14
|
+
*
|
|
15
|
+
* Example (baseDelayMs = 1000):
|
|
16
|
+
* Try 1: 250ms – 1,000ms
|
|
17
|
+
* Try 2: 250ms – 2,000ms
|
|
18
|
+
* Try 3: 250ms – 4,000ms
|
|
19
|
+
* Try 4: 250ms – 8,000ms
|
|
20
|
+
* Try 5+: 250ms – maxDelayMs
|
|
21
|
+
*/
|
|
22
|
+
export function exponentialBackoffFullJitter(
|
|
23
|
+
tries: number,
|
|
24
|
+
{
|
|
25
|
+
baseDelayMs,
|
|
26
|
+
maxDelayMs,
|
|
27
|
+
minDelayMs = 250,
|
|
28
|
+
}: {
|
|
29
|
+
baseDelayMs: number;
|
|
30
|
+
maxDelayMs: number;
|
|
31
|
+
minDelayMs?: number;
|
|
32
|
+
},
|
|
33
|
+
): number {
|
|
34
|
+
const exponentialDelay = Math.min(
|
|
35
|
+
maxDelayMs,
|
|
36
|
+
baseDelayMs * Math.pow(2, Math.max(0, tries - 1)),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (exponentialDelay <= minDelayMs) {
|
|
40
|
+
return minDelayMs;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const jitterRange = exponentialDelay - minDelayMs;
|
|
44
|
+
return minDelayMs + Math.floor(Math.random() * jitterRange);
|
|
45
|
+
}
|
package/src/interfaces.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface KnockOptions {
|
|
|
9
9
|
host?: string;
|
|
10
10
|
logLevel?: LogLevel;
|
|
11
11
|
branch?: string;
|
|
12
|
+
/** Automatically disconnect the socket when the page is hidden and reconnect when visible. Defaults to `true`. */
|
|
13
|
+
disconnectOnPageHidden?: boolean;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export interface KnockObject<T = GenericData> {
|