Skip to content

Commit b0189f7

Browse files
committed
🐛 Refactor populateEdgesWithTotalVisits to fix dropoff compute edge cases
1 parent 3c8cc89 commit b0189f7

File tree

3 files changed

+96
-55
lines changed

3 files changed

+96
-55
lines changed

apps/builder/src/features/analytics/helpers/populateEdgesWithTotalVisits.ts

Lines changed: 83 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import type {
1111
Target,
1212
} from "@typebot.io/typebot/schemas/edge";
1313
import type { EdgeWithTotalVisits, TotalAnswers } from "../schemas";
14+
import type {
15+
DropoffLogger,
16+
TraversalFrame,
17+
VisitedPathsByEdge,
18+
} from "../types";
1419
import { getVisitedEdgeToPropFromId } from "./getVisitedEdgeToPropFromId";
1520

16-
type Logger = (msg: string, ctx?: Record<string, unknown>) => void;
17-
18-
type Frame = { edgeId: string; totalUsers: number; depth: number };
19-
2021
type Params = {
2122
initialEdge: {
2223
id: string;
@@ -26,7 +27,7 @@ type Params = {
2627
edges: Edge[];
2728
groups: GroupV6[];
2829
totalAnswers: TotalAnswers[];
29-
logger?: Logger;
30+
logger?: DropoffLogger;
3031
};
3132

3233
export function populateEdgesWithTotalVisits({
@@ -37,97 +38,114 @@ export function populateEdgesWithTotalVisits({
3738
totalAnswers,
3839
logger,
3940
}: Params): EdgeWithTotalVisits[] {
40-
const edgesById = new Map(edges.map((e) => [e.id, e]));
41-
const groupsById = new Map(groups.map((g) => [g.id, g]));
41+
const edgesById = new Map(edges.map((edge) => [edge.id, edge]));
42+
const groupsById = new Map(groups.map((group) => [group.id, group]));
4243
const totalAnswersByInputBlockId = new Map(
43-
totalAnswers.map((t) => [t.blockId, t.total]),
44+
totalAnswers.map((answer) => [answer.blockId, answer.total]),
4445
);
4546

46-
const offDefaultPathEdgeIds = new Set(
47-
offDefaultPathEdgeWithTotalVisits.map((e) => e.id),
47+
const offPathEdgeIds = new Set(
48+
offDefaultPathEdgeWithTotalVisits.map((offPathEdge) => offPathEdge.id),
4849
);
49-
const totals = new Map<string, number>(
50-
offDefaultPathEdgeWithTotalVisits.map((e) => [e.id, e.total]),
50+
const edgeTotalsById = new Map(
51+
offDefaultPathEdgeWithTotalVisits.map((offPathEdge) => [
52+
offPathEdge.id,
53+
offPathEdge.total,
54+
]),
5155
);
5256

53-
const visited = new Set<string>();
57+
const visitedByEdge: VisitedPathsByEdge = new Map();
5458

55-
const stack: Frame[] = [
56-
{ edgeId: initialEdge.id, totalUsers: initialEdge.total, depth: 0 },
59+
const depthFirstFrames: TraversalFrame[] = [
60+
{
61+
edgeId: initialEdge.id,
62+
usersRemaining: initialEdge.total,
63+
pathIndex: 0,
64+
},
5765
];
5866

59-
while (stack.length) {
60-
const { edgeId, totalUsers, depth } = stack.pop()!;
67+
while (depthFirstFrames.length) {
68+
visitFrame(depthFirstFrames.pop()!);
69+
}
70+
71+
return [...edgeTotalsById.entries()].map(([id, total]) => ({
72+
id,
73+
total,
74+
to: getVisitedEdgeToPropFromId(id, { edges }),
75+
}));
76+
77+
/* ================================================================ */
78+
/* Inner helpers */
79+
/* ================================================================ */
80+
function visitFrame({ edgeId, usersRemaining, pathIndex }: TraversalFrame) {
81+
if (usersRemaining <= 0) return;
6182

62-
if (totalUsers <= 0) continue;
83+
if (markVisited(visitedByEdge, edgeId, pathIndex)) return;
6384

64-
if (!offDefaultPathEdgeIds.has(edgeId)) {
65-
totals.set(edgeId, (totals.get(edgeId) ?? 0) + totalUsers);
85+
if (!offPathEdgeIds.has(edgeId)) {
86+
edgeTotalsById.set(
87+
edgeId,
88+
(edgeTotalsById.get(edgeId) ?? 0) + usersRemaining,
89+
);
6690
}
6791

68-
if (visited.has(edgeId)) continue;
69-
visited.add(edgeId);
70-
7192
logger?.(
7293
`▶︎ visiting ${edgeIdToHumanReadableLabel(edgeId, {
7394
edges,
7495
groups,
7596
offDefaultPathEdgeWithTotalVisits,
7697
})}`,
7798
{
78-
totalUsers,
79-
depth,
99+
usersRemaining,
80100
},
81101
);
82102

83103
const edge = edgesById.get(edgeId);
84-
if (!edge?.to) continue;
104+
if (!edge?.to) return;
85105

86106
const group = groupsById.get(edge.to.groupId);
87-
if (!group) continue;
107+
if (!group) return;
88108

89-
let remainingForNextDefaultOutgoingEdge = totalUsers;
109+
let remainingForNextDefaultOutgoingEdge = usersRemaining;
90110

111+
let nextPathIndexIncrement = 1;
91112
for (const block of sliceFrom(group.blocks, edge.to.blockId)) {
92-
if (isInputBlock(block))
113+
if (isInputBlock(block)) {
93114
remainingForNextDefaultOutgoingEdge =
94115
totalAnswersByInputBlockId.get(block.id) ?? 0;
116+
totalAnswersByInputBlockId.delete(block.id);
117+
}
95118

96119
for (const itemEdgeId of outgoingItemEdges(block)) {
97-
const itemTotal = totals.get(itemEdgeId);
98-
if (itemTotal) {
99-
enqueue(itemEdgeId, itemTotal, depth + 1);
120+
const itemTotal = edgeTotalsById.get(itemEdgeId);
121+
if (itemTotal && itemTotal > 0) {
122+
enqueue(itemEdgeId, itemTotal, pathIndex + nextPathIndexIncrement);
123+
nextPathIndexIncrement++;
100124
remainingForNextDefaultOutgoingEdge -= itemTotal;
101125
}
102126
}
103127

104128
if (isJump(block)) {
105129
const virtualId = createVirtualEdgeId(block.options);
106-
const virtualTotal = totals.get(virtualId);
107-
if (virtualTotal) {
108-
enqueue(virtualId, virtualTotal, depth + 1);
130+
const virtualTotal = edgeTotalsById.get(virtualId);
131+
if (virtualTotal && virtualTotal > 0) {
132+
enqueue(virtualId, virtualTotal, pathIndex + 1);
109133
}
110134
}
111135

112136
if (block.outgoingEdgeId) {
113137
enqueue(
114138
block.outgoingEdgeId,
115139
remainingForNextDefaultOutgoingEdge,
116-
depth + 1,
140+
pathIndex,
117141
);
118142
}
119143
}
120144
}
121145

122-
return [...totals.entries()].map(([id, total]) => ({
123-
id,
124-
total,
125-
to: getVisitedEdgeToPropFromId(id, { edges }),
126-
}));
127-
128-
function enqueue(id: string, totalUsers: number, depth: number) {
129-
if (totalUsers <= 0 || visited.has(id)) return;
130-
stack.push({ edgeId: id, totalUsers, depth });
146+
function enqueue(edgeId: string, usersRemaining: number, pathIndex: number) {
147+
if (usersRemaining <= 0) return;
148+
depthFirstFrames.push({ edgeId, usersRemaining, pathIndex });
131149
}
132150
}
133151

@@ -136,10 +154,11 @@ const sliceFrom = (blocks: Block[], startId?: string) =>
136154

137155
const outgoingItemEdges = (block: Block) => {
138156
if (!blockHasItems(block)) return [];
139-
return (
140-
block.items?.flatMap((i) => (i.outgoingEdgeId ? [i.outgoingEdgeId] : [])) ??
141-
[]
142-
);
157+
const ids: string[] = [];
158+
for (const item of block.items ?? []) {
159+
if (item.outgoingEdgeId) ids.push(item.outgoingEdgeId);
160+
}
161+
return ids;
143162
};
144163

145164
const isJump = (
@@ -191,3 +210,18 @@ const edgeIdToHumanReadableLabel = (
191210
label += "]";
192211
return label;
193212
};
213+
214+
const markVisited = (
215+
visitedByEdge: VisitedPathsByEdge,
216+
edgeId: string,
217+
pathIdx: number,
218+
): boolean => {
219+
let paths = visitedByEdge.get(edgeId);
220+
if (!paths) {
221+
paths = new Set<number>();
222+
visitedByEdge.set(edgeId, paths);
223+
}
224+
if (paths.has(pathIdx)) return true;
225+
paths.add(pathIdx);
226+
return false;
227+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export type DropoffLogger = (
2+
msg: string,
3+
ctx?: Record<string, unknown>,
4+
) => void;
5+
6+
export type TraversalFrame = {
7+
edgeId: string;
8+
usersRemaining: number;
9+
pathIndex: number;
10+
};
11+
12+
export type VisitedPathsByEdge = Map<string, Set<number>>;

apps/builder/src/lib/trpc.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { AppRouter } from "@/helpers/server/routers/appRouter";
2-
import { createTRPCProxyClient, httpBatchLink, loggerLink } from "@trpc/client";
2+
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
33
import { createTRPCNext } from "@trpc/next";
44
import { env } from "@typebot.io/env";
55
import superjson from "superjson";
@@ -11,11 +11,6 @@ export const trpc = createTRPCNext<AppRouter>({
1111
config() {
1212
return {
1313
links: [
14-
loggerLink({
15-
enabled: (opts) =>
16-
process.env.NODE_ENV === "development" ||
17-
(opts.direction === "down" && opts.result instanceof Error),
18-
}),
1914
httpBatchLink({
2015
url: `${getBaseUrl()}/api/trpc`,
2116
}),

0 commit comments

Comments
 (0)