Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/guide-example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ function App() {
readyToTarget={true}
listenForUpdates={true}
colorMode={colorMode}
toolbar="v2"
>
<div style={{ padding: "1rem 2rem" }}>
<h1>Knock In-App Guide Example</h1>
Expand Down
57 changes: 50 additions & 7 deletions packages/client/src/clients/guide/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,15 @@ export class KnockGuideClient {
) {
const {
trackLocationFromWindow = true,
// TODO(KNO-11523): Remove once we ship guide toolbar v2, and offload as
// much debugging specific logic and responsibilities to toolbar.
trackDebugParams = false,
throttleCheckInterval = DEFAULT_COUNTER_INCREMENT_INTERVAL,
} = options;
const win = checkForWindow();

const location = trackLocationFromWindow ? win?.location?.href : undefined;

const debug = detectDebugParams();
const debug = trackDebugParams ? detectDebugParams() : undefined;

this.store = new Store<StoreState>({
guideGroups: [],
Expand Down Expand Up @@ -412,8 +414,9 @@ export class KnockGuideClient {
const params = {
...this.targetParams,
user_id: this.knock.userId,
force_all_guides: debugState.forcedGuideKey ? true : undefined,
preview_session_id: debugState.previewSessionId || undefined,
force_all_guides:
debugState?.forcedGuideKey || debugState?.debugging ? true : undefined,
preview_session_id: debugState?.previewSessionId || undefined,
};

const newChannel = this.socket.channel(this.socketChannelTopic, params);
Expand Down Expand Up @@ -561,6 +564,39 @@ export class KnockGuideClient {
}
}

setDebug(debugOpts?: Omit<DebugState, "debugging">) {
this.knock.log("[Guide] .setDebug()");
const shouldRefetch = !this.store.state.debug?.debugging;

this.store.setState((state) => ({
...state,
debug: { ...debugOpts, debugging: true },
}));

if (shouldRefetch) {
this.knock.log(
`[Guide] Start debugging, refetching guides and resubscribing to the websocket channel`,
);
this.fetch();
this.subscribe();
}
}

unsetDebug() {
this.knock.log("[Guide] .unsetDebug()");
const shouldRefetch = this.store.state.debug?.debugging;

this.store.setState((state) => ({ ...state, debug: undefined }));

if (shouldRefetch) {
this.knock.log(
`[Guide] Stop debugging, refetching guides and resubscribing to the websocket channel`,
);
this.fetch();
this.subscribe();
}
}

//
// Store selector
//
Expand Down Expand Up @@ -924,7 +960,7 @@ export class KnockGuideClient {
// Get the next unarchived step.
getStep() {
// If debugging this guide, return the first step regardless of archive status
if (self.store.state.debug.forcedGuideKey === this.key) {
if (self.store.state.debug?.forcedGuideKey === this.key) {
return this.steps[0];
}

Expand Down Expand Up @@ -981,7 +1017,7 @@ export class KnockGuideClient {

// Append debug params
const debugState = this.store.state.debug;
if (debugState.forcedGuideKey) {
if (debugState?.forcedGuideKey || debugState?.debugging) {
combinedParams.force_all_guides = true;
}

Expand Down Expand Up @@ -1150,8 +1186,15 @@ export class KnockGuideClient {

this.knock.log(`[Guide] Detected a location change: ${href}`);

if (!this.options.trackDebugParams) {
this.setLocation(href);
return;
}

// TODO(KNO-11523): Remove below once we ship toolbar v2.

// If entering debug mode, fetch all guides.
const currentDebugParams = this.store.state.debug;
const currentDebugParams = this.store.state.debug || {};
const newDebugParams = detectDebugParams();

this.setLocation(href, { debug: newDebugParams });
Expand Down
4 changes: 3 additions & 1 deletion packages/client/src/clients/guide/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export type QueryStatus = {
};

export type DebugState = {
debugging?: boolean;
forcedGuideKey?: string | null;
previewSessionId?: string | null;
};
Expand All @@ -218,7 +219,7 @@ export type StoreState = {
queries: Record<QueryKey, QueryStatus>;
location: string | undefined;
counter: number;
debug: DebugState;
debug?: DebugState;
Copy link
Contributor Author

@thomaswhyyou thomaswhyyou Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just made the debug field optional, since it's only relevant with the toolbar / debugging.

};

export type QueryFilterParams = Pick<GetGuidesQueryParams, "type">;
Expand All @@ -241,6 +242,7 @@ export type TargetParams = {

export type ConstructorOpts = {
trackLocationFromWindow?: boolean;
trackDebugParams?: boolean;
orderResolutionDuration?: number;
throttleCheckInterval?: number;
};
Expand Down
126 changes: 120 additions & 6 deletions packages/client/test/clients/guide/guide.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ describe("KnockGuideClient", () => {
queries: {},
location: undefined,
counter: 0,
debug: { forcedGuideKey: null, previewSessionId: null },
debug: undefined,
});
});

Expand Down Expand Up @@ -194,7 +194,7 @@ describe("KnockGuideClient", () => {
queries: {},
location: "https://example.com",
counter: 0,
debug: { forcedGuideKey: null, previewSessionId: null },
debug: undefined,
});
});

Expand All @@ -211,7 +211,7 @@ describe("KnockGuideClient", () => {
queries: {},
location: undefined,
counter: 0,
debug: { forcedGuideKey: null, previewSessionId: null },
debug: undefined,
});
});

Expand All @@ -234,7 +234,7 @@ describe("KnockGuideClient", () => {
});

expect(() => {
new KnockGuideClient(mockKnock, channelId);
new KnockGuideClient(mockKnock, channelId, {}, { trackDebugParams: true });
}).not.toThrow();

expect(mockLocalStorageWithErrors.setItem).toHaveBeenCalled();
Expand Down Expand Up @@ -2959,7 +2959,7 @@ describe("KnockGuideClient", () => {
mockKnock,
channelId,
{},
{ trackLocationFromWindow: true },
{ trackLocationFromWindow: true, trackDebugParams: true },
);

client.store.state.debug = { forcedGuideKey: null };
Expand Down Expand Up @@ -3016,7 +3016,7 @@ describe("KnockGuideClient", () => {
mockKnock,
channelId,
{},
{ trackLocationFromWindow: true },
{ trackLocationFromWindow: true, trackDebugParams: true },
);

client.store.state.debug = { forcedGuideKey: "test_guide" };
Expand Down Expand Up @@ -3271,4 +3271,118 @@ describe("KnockGuideClient", () => {
);
});
});

describe("setDebug", () => {
test("sets debug state with debugging: true", () => {
const client = new KnockGuideClient(mockKnock, channelId);
client.store.state.debug = undefined;

const fetchSpy = vi
.spyOn(client, "fetch")
.mockImplementation(() => Promise.resolve({ status: "ok" }));
const subscribeSpy = vi
.spyOn(client, "subscribe")
.mockImplementation(() => {});

client.setDebug();

expect(client.store.state.debug!.debugging!).toBe(true);

// calls fetch and subscribe when not already debugging
expect(fetchSpy).toHaveBeenCalled();
expect(subscribeSpy).toHaveBeenCalled();
});

test("sets debug state with provided options", () => {
const client = new KnockGuideClient(mockKnock, channelId);
client.store.state.debug = undefined;

vi.spyOn(client, "fetch").mockImplementation(() => Promise.resolve({ status: "ok" }));
vi.spyOn(client, "subscribe").mockImplementation(() => {});

client.setDebug({ forcedGuideKey: "test_guide" });

expect(client.store.state.debug!.debugging!).toBe(true);
expect(client.store.state.debug!.forcedGuideKey!).toBe("test_guide");
});

test("does not call fetch and subscribe when already debugging", () => {
const client = new KnockGuideClient(mockKnock, channelId);
client.store.state.debug = { debugging: true };

const fetchSpy = vi
.spyOn(client, "fetch")
.mockImplementation(() => Promise.resolve({ status: "ok" }));
const subscribeSpy = vi
.spyOn(client, "subscribe")
.mockImplementation(() => {});

client.setDebug();

expect(client.store.state.debug!.debugging!).toBe(true);

expect(fetchSpy).not.toHaveBeenCalled();
expect(subscribeSpy).not.toHaveBeenCalled();
});
});

describe("unsetDebug", () => {
test("sets debug state to undefined", () => {
const client = new KnockGuideClient(mockKnock, channelId);
client.store.state.debug = { debugging: true };

const fetchSpy = vi
.spyOn(client, "fetch")
.mockImplementation(() => Promise.resolve({ status: "ok" }));
const subscribeSpy = vi
.spyOn(client, "subscribe")
.mockImplementation(() => {});

client.unsetDebug();

expect(client.store.state.debug).toBe(undefined);

// calls fetch and subscribe when was debugging
expect(fetchSpy).toHaveBeenCalled();
expect(subscribeSpy).toHaveBeenCalled();
});

test("does not call fetch and subscribe when was not debugging", () => {
const client = new KnockGuideClient(mockKnock, channelId);
client.store.state.debug = undefined;

const fetchSpy = vi
.spyOn(client, "fetch")
.mockImplementation(() => Promise.resolve({ status: "ok" }));
const subscribeSpy = vi
.spyOn(client, "subscribe")
.mockImplementation(() => {});

client.unsetDebug();

expect(client.store.state.debug).toBe(undefined);

expect(fetchSpy).not.toHaveBeenCalled();
expect(subscribeSpy).not.toHaveBeenCalled();
});

test("does not call fetch and subscribe when debug exists but debugging is false", () => {
const client = new KnockGuideClient(mockKnock, channelId);
client.store.state.debug = { debugging: false };

const fetchSpy = vi
.spyOn(client, "fetch")
.mockImplementation(() => Promise.resolve({ status: "ok" }));
const subscribeSpy = vi
.spyOn(client, "subscribe")
.mockImplementation(() => {});

client.unsetDebug();

expect(client.store.state.debug).toBe(undefined);

expect(fetchSpy).not.toHaveBeenCalled();
expect(subscribeSpy).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type KnockGuideProviderProps = {
colorMode?: ColorMode;
targetParams?: KnockGuideTargetParams;
trackLocationFromWindow?: boolean;
trackDebugParams?: boolean;
orderResolutionDuration?: number; // in milliseconds
throttleCheckInterval?: number; // in milliseconds
};
Expand All @@ -36,6 +37,10 @@ export const KnockGuideProvider: React.FC<
colorMode = "light",
targetParams = {},
trackLocationFromWindow = true,
// Whether the guide client should look for debug params in url/local storage
// to launch guide toolbar. Set to true if using toolbar v1.
// TODO(KNO-11523): Remove this once we ship v2.
trackDebugParams = false,
// Default to 0 which works well for react apps as this "yields" to react for
// one render cyle first and close the group stage.
orderResolutionDuration = 0,
Expand All @@ -55,6 +60,7 @@ export const KnockGuideProvider: React.FC<
const knockGuideClient = React.useMemo(() => {
return new KnockGuideClient(knock, channelId, stableTargetParams, {
trackLocationFromWindow,
trackDebugParams,
orderResolutionDuration,
throttleCheckInterval,
});
Expand All @@ -63,6 +69,7 @@ export const KnockGuideProvider: React.FC<
channelId,
stableTargetParams,
trackLocationFromWindow,
trackDebugParams,
orderResolutionDuration,
throttleCheckInterval,
]);
Expand Down
1 change: 0 additions & 1 deletion packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export {
Card,
CardView,
KnockGuideProvider,
GuideToolbar as KnockGuideToolbar,
Modal,
ModalView,
} from "./modules/guide";
Expand Down

This file was deleted.

Loading
Loading