Skip to content

Commit

Permalink
feat: debug for tooltip error
Browse files Browse the repository at this point in the history
  • Loading branch information
VojtechVidra committed Jan 30, 2024
1 parent e6017d7 commit 40b0dce
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 49 deletions.
17 changes: 17 additions & 0 deletions workspaces/e2e/tests/error/error.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/tests/reset.css" />
<link rel="stylesheet" href="/node_modules/@flows/js/css.min/flows.css" />
</head>
<body>
<button class="add-target">Add target</button>
<button class="remove-target">Remove target</button>
<button class="start-flow">Start flow</button>
<div class="log"></div>

<script type="module" src="./error.ts"></script>
</body>
</html>
23 changes: 23 additions & 0 deletions workspaces/e2e/tests/error/error.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { expect, test } from "@playwright/test";

test("Emits error event", async ({ page }) => {
await page.goto("/error/error.html");
await page.locator(".start-flow").click();
await expect(page.locator("[data-type='tooltipError']")).toHaveCount(1);
await expect(page.locator("[data-type='invalidateTooltipError']")).toHaveCount(0);
});

test("should invalidate error without issues", async ({ page }) => {
await page.goto("/error/error.html");
await page.locator(".start-flow").click();
await expect(page.locator("[data-type='tooltipError']")).toHaveCount(1);
await page.locator(".add-target").click();
await page.locator(".remove-target").click();
await page.locator(".add-target").click();
await expect(page.locator("[data-type='tooltipError']")).toHaveCount(1);
await expect(page.locator("[data-type='invalidateTooltipError']")).toHaveAttribute(
"data-reference-id",
"0",
);
await expect(page.locator("[data-type='invalidateTooltipError']")).toHaveCount(1);
});
48 changes: 48 additions & 0 deletions workspaces/e2e/tests/error/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { init, startFlow } from "@flows/js";

let counter = 0;

init({
flows: [
{
id: "flow",
steps: [
{
element: ".target",
title: "Hello",
},
],
},
],
_debug: async (e) => {
const p = document.createElement("p");
p.classList.add("log-item");
p.dataset.type = e.type;
p.dataset.referenceId = e.referenceId;
p.innerText = JSON.stringify(e);
document.querySelector(".log")?.appendChild(p);
const referenceId = counter.toString();
counter = counter + 1;
return { referenceId };
},
});

const createTarget = (): void => {
const el = document.createElement("div");
el.classList.add("target");
el.style.width = "20px";
el.style.height = "20px";
el.style.background = "grey";
el.style.margin = "40px";
document.body.appendChild(el);
};
document.querySelector(".add-target")?.addEventListener("click", () => {
createTarget();
});
document.querySelector(".remove-target")?.addEventListener("click", () => {
document.querySelector(".target")?.remove();
});

document.querySelector(".start-flow")?.addEventListener("click", () => {
startFlow("flow");
});
3 changes: 2 additions & 1 deletion workspaces/js/src/cloud/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ export const api = (baseUrl: string) => ({
stepIndex?: string;
stepHash?: string;
flowHash: string;
}): Promise<void> =>
}): Promise<{ id: string }> =>
f(`${baseUrl}/sdk/events`, {
method: "POST",
body,
}),
deleteEvent: (eventId: string) => f(`${baseUrl}/sdk/events/${eventId}`, { method: "DELETE" }),
getFlows: ({ projectId, userHash }: { projectId: string; userHash?: string }): Promise<Flow[]> =>
f(`${baseUrl}/sdk/flows?projectId=${projectId}${userHash ? `&userHash=${userHash}` : ""}`),
getPreviewFlow: ({ flowId, projectId }: { projectId: string; flowId: string }): Promise<Flow> =>
Expand Down
51 changes: 31 additions & 20 deletions workspaces/js/src/cloud/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { startFlow } from "../public-methods";
import { init as flowsInit } from "../init";
import type { FlowsCloudOptions } from "../types";
import type { DebugEvent, FlowsCloudOptions, TrackingEvent } from "../types";
import { hash } from "../utils";
import { log } from "../log";
import { validateFlowsOptions, validateCloudFlowsOptions } from "../validation";
Expand Down Expand Up @@ -39,29 +39,40 @@ export const init = async (options: FlowsCloudOptions): Promise<void> => {
);
});

const saveEvent = async (
event: DebugEvent | TrackingEvent,
): Promise<{ referenceId: string } | undefined> => {
const { flowHash, flowId, type, projectId = "", stepIndex, stepHash, userId } = event;

return api(apiUrl)
.sendEvent({
eventTime: new Date().toISOString(),
flowHash,
flowId,
projectId,
type,
stepHash,
stepIndex: stepIndex?.toString(),
userHash: userId ? await hash(userId) : undefined,
})
.then((res) => ({ referenceId: res.id }))
.catch((err) => {
log.error("Failed to send event to cloud\n", err);
return undefined;
});
};

return flowsInit({
...options,
flows: [...(options.flows ?? []), ...(flows || [])],
tracking: (event) => {
options.tracking?.(event);

const { flowHash, flowId, type, projectId = "", stepIndex, stepHash, userId } = event;

void (async () =>
api(apiUrl)
.sendEvent({
eventTime: new Date().toISOString(),
flowHash,
flowId,
projectId,
type,
stepHash,
stepIndex: stepIndex?.toString(),
userHash: userId ? await hash(userId) : undefined,
})
.catch((err) => {
log.error("Failed to send event to cloud\n", err);
}))();
void saveEvent(event);
},
_debug: async (event) => {
if (event.type === "invalidateTooltipError") {
if (event.referenceId) void api(apiUrl).deleteEvent(event.referenceId);
} else return saveEvent(event);
},
onLocationChange: (pathname, context) => {
const params = new URLSearchParams(pathname.split("?")[1] ?? "");
Expand All @@ -83,7 +94,7 @@ export const init = async (options: FlowsCloudOptions): Promise<void> => {
void api(apiUrl)
.getFlowDetail({ flowId, projectId: options.projectId })
.then((flow) => {
context.addFlowData({ ...flow, draft: true });
context.addFlowData(flow);
})
.catch((err) => {
log.error("Failed to load flow detail\n", err);
Expand Down
67 changes: 45 additions & 22 deletions workspaces/js/src/flow-state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { FlowsContext } from "./flows-context";
import { render } from "./render";
import type { Flow, FlowStep, FlowStepIndex, TrackingEvent } from "./types";
import type { DebugEvent, Flow, FlowStep, FlowStepIndex, TrackingEvent } from "./types";
import { hash, isModalStep, isTooltipStep } from "./utils";

const getStep = ({ flow, step }: { flow: Flow; step: FlowStepIndex }): FlowStep | undefined => {
Expand All @@ -14,6 +14,7 @@ export class FlowState {
flowId: string;
flowElement?: { element: HTMLElement; cleanup?: () => void; target?: Element };
waitingForElement = false;
tooltipErrorPromise?: Promise<{ referenceId: string } | undefined> | null;

flowsContext: FlowsContext;

Expand All @@ -23,6 +24,7 @@ export class FlowState {
}
set stepHistory(value: FlowStepIndex[]) {
this._stepHistory = value;
this.enterStep();
this.flowsContext.savePersistentState();
}

Expand All @@ -33,11 +35,8 @@ export class FlowState {
?.stepHistory ?? [0];
if (this.flow?._incompleteSteps)
this.flowsContext.onIncompleteFlowStart?.(this.flowId, this.flowsContext);
this.track({ type: "startFlow" });
}

get storageKey(): string {
return `flows.${this.flowId}.stepHistory`;
void this.track({ type: "startFlow" });
this.enterStep();
}

get step(): FlowStepIndex {
Expand All @@ -49,19 +48,34 @@ export class FlowState {
return getStep({ flow: this.flow, step: this.step });
}

track(props: Pick<TrackingEvent, "type">): this {
if (this.flow?.draft) return this;
async track(props: Pick<TrackingEvent, "type">): Promise<void> {
if (!this.flow || this.flow.draft) return;

void (async () => {
if (!this.flow) return;
this.flowsContext.track({
flowId: this.flowId,
stepIndex: this.step,
stepHash: this.currentStep ? await hash(JSON.stringify(this.currentStep)) : undefined,
flowHash: await hash(JSON.stringify(this.flow)),
...props,
});
})();
this.flowsContext.track({
flowId: this.flowId,
stepIndex: this.step,
stepHash: this.currentStep ? await hash(JSON.stringify(this.currentStep)) : undefined,
flowHash: await hash(JSON.stringify(this.flow)),
...props,
});
}
async debug(
props: Pick<DebugEvent, "type" | "referenceId">,
): Promise<{ referenceId: string } | undefined> {
if (!this.flow || this.flow.draft) return;

return this.flowsContext.handleDebug({
flowId: this.flowId,
stepIndex: this.step,
stepHash: this.currentStep ? await hash(JSON.stringify(this.currentStep)) : undefined,
flowHash: await hash(JSON.stringify(this.flow)),
...props,
});
}
enterStep(): this {
const step = this.currentStep;
if (step && isTooltipStep(step) && !this.tooltipErrorPromise)
this.tooltipErrorPromise = this.debug({ type: "tooltipError" });

return this;
}
Expand Down Expand Up @@ -91,7 +105,7 @@ export class FlowState {
this.stepHistory = [...this.stepHistory, newStepIndex];

if (this.currentStep) this.flowsContext.onNextStep?.(this.currentStep);
this.track({ type: "nextStep" });
void this.track({ type: "nextStep" });
return this;
}
get hasNextStep(): boolean {
Expand Down Expand Up @@ -123,7 +137,7 @@ export class FlowState {
)
this.stepHistory = this.stepHistory.slice(0, -1);
if (this.currentStep) this.flowsContext.onPrevStep?.(this.currentStep);
this.track({ type: "prevStep" });
void this.track({ type: "prevStep" });
return this;
}
get hasPrevStep(): boolean {
Expand All @@ -142,18 +156,27 @@ export class FlowState {
}

render(this);

if (step && isTooltipStep(step) && !this.waitingForElement && this.tooltipErrorPromise) {
const tooltipErrorPromise = this.tooltipErrorPromise;
this.tooltipErrorPromise = null;
void tooltipErrorPromise.then((res) =>
this.debug({ type: "invalidateTooltipError", referenceId: res?.referenceId }),
);
}

return this;
}

cancel(): this {
this.track({ type: "cancelFlow" });
void this.track({ type: "cancelFlow" });
this.flowsContext.flowSeen(this.flowId);
this.unmount();
return this;
}

finish(): this {
this.track({ type: "finishFlow" });
void this.track({ type: "finishFlow" });
this.flowsContext.flowSeen(this.flowId);
this.unmount();
return this;
Expand Down
28 changes: 23 additions & 5 deletions workspaces/js/src/flows-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import type {
UserProperties,
ImmutableMap,
FlowStepIndex,
Tracking,
Debug,
DebugEvent,
} from "./types";

interface PersistentState {
Expand Down Expand Up @@ -67,7 +70,9 @@ export class FlowsContext {
}
startInstancesFromLocalStorage(): this {
this.persistentState.instances.forEach((instance) => {
if (this.#instances.has(instance.flowId) || !this.flowsById?.[instance.flowId]) return;
if (!this.flowsById?.[instance.flowId]) return;
const runningInstance = this.#instances.get(instance.flowId);
if (runningInstance) return runningInstance.render();
const state = new FlowState(instance.flowId, this);
this.#instances.set(instance.flowId, state);
state.render();
Expand All @@ -81,7 +86,8 @@ export class FlowsContext {
flowsById?: Record<string, Flow>;
onNextStep?: (step: FlowStep) => void;
onPrevStep?: (step: FlowStep) => void;
tracking?: (event: TrackingEvent) => void;
tracking?: Tracking;
debug?: Debug;
onSeenFlowIdsChange?: (seenFlowIds: string[]) => void;
rootElement?: string;
onLocationChange?: (pathname: string, context: FlowsContext) => void;
Expand All @@ -92,6 +98,7 @@ export class FlowsContext {
this.onNextStep = options.onNextStep;
this.onPrevStep = options.onPrevStep;
this.tracking = options.tracking;
this.debug = options._debug;
this.seenFlowIds = [...(options.seenFlowIds ?? [])];
this.onSeenFlowIdsChange = options.onSeenFlowIdsChange;
this.onLocationChange = options.onLocationChange;
Expand Down Expand Up @@ -124,16 +131,27 @@ export class FlowsContext {
return this;
}

track(props: Omit<TrackingEvent, "userId" | "projectId" | "location">): this {
if (!this.tracking) return this;
track(props: Omit<TrackingEvent, "userId" | "projectId" | "location">): void {
if (!this.tracking) return;
const event: TrackingEvent = {
userId: this.userId,
location: getPathname(),
...props,
};
if (this.projectId) event.projectId = this.projectId;
this.tracking(event);
return this;
}
handleDebug(
props: Omit<DebugEvent, "userId" | "projectId" | "location">,
): undefined | ReturnType<Debug> {
if (!this.debug) return;
const event: DebugEvent = {
userId: this.userId,
location: getPathname(),
...props,
};
if (this.projectId) event.projectId = this.projectId;
return this.debug(event);
}

flowSeen(flowId: string): this {
Expand Down
Loading

0 comments on commit 40b0dce

Please sign in to comment.