Skip to content

Commit b30bfa5

Browse files
committed
fix(webapp): apply active filters to the new-deliveries live count
The "N new deliveries" badge queried the live count with only the endpoint and time window, dropping the status, webhook, delivery-id and test filters the list was showing, so on a filtered list it announced deliveries the list would never display. The count now runs through the same filter resolution as the list (WebhookDeliveriesListPresenter.countNewDeliveries), and the live-reload hook forwards the active filter params, so the badge only counts rows the list shows.
1 parent d1b7691 commit b30bfa5

3 files changed

Lines changed: 152 additions & 34 deletions

File tree

apps/webapp/app/components/webhookDeliveries/v1/useDeliveriesLiveReload.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,15 @@ export function useDeliveriesLiveReload({
195195
if (checkForNewDeliveries) {
196196
searchParams.set("includeNewDeliveries", "true");
197197
searchParams.set("since", String(knownNewestDeliveryMs));
198-
const to = new URLSearchParams(location.search).get("to");
198+
const current = new URLSearchParams(location.search);
199+
const to = current.get("to");
199200
if (to) searchParams.set("to", to);
201+
for (const status of current.getAll("statuses")) searchParams.append("statuses", status);
202+
for (const webhook of current.getAll("webhooks")) searchParams.append("webhooks", webhook);
203+
for (const key of ["deliveryId", "runId", "test"] as const) {
204+
const value = current.get(key);
205+
if (value) searchParams.set(key, value);
206+
}
200207
}
201208

202209
deliveriesPollFetcher.load(`${deliveriesResourcesBasePath}/live?${searchParams.toString()}`);

apps/webapp/app/presenters/v3/WebhookDeliveriesListPresenter.server.ts

Lines changed: 102 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -62,32 +62,11 @@ export class WebhookDeliveriesListPresenter {
6262
}): Promise<WebhookDeliveriesListResult> {
6363
const periodMs = period ? (parseDuration(period) ?? undefined) : undefined;
6464

65-
// Resolve the handler-slug webhook filter to endpoint ids. A non-empty webhook
66-
// filter that matches no endpoints must return nothing, so fall back to a
67-
// sentinel id that can never match rather than dropping the filter entirely.
68-
let webhookEndpointIds: string[] | undefined;
69-
if (webhooks && webhooks.length > 0) {
70-
const endpoints = await webhookReplica.webhookEndpoint.findMany({
71-
where: {
72-
runtimeEnvironmentId: environmentId,
73-
handlerWebhookId: { in: boundedIn(webhooks) },
74-
},
75-
select: { id: true },
76-
});
77-
webhookEndpointIds = endpoints.length > 0 ? endpoints.map((e) => e.id) : ["__none__"];
78-
}
79-
80-
// The runId param is a FRIENDLY run id; the deliveries store the INTERNAL id.
81-
// Resolve it, and force empty results when no run matches.
82-
let internalRunId: string | undefined;
83-
if (runId) {
84-
const run = await runStore.findRun(
85-
{ friendlyId: runId },
86-
{ select: { id: true } },
87-
this.replica
88-
);
89-
internalRunId = run?.id ?? "__none__";
90-
}
65+
const { webhookEndpointIds, internalRunId } = await this.#resolveFilterScope(
66+
environmentId,
67+
webhooks,
68+
runId
69+
);
9170

9271
// Built per request (factory, NOT a singleton), matching every RunsRepository consumer.
9372
const repository = webhookDeliveriesRepository({
@@ -157,4 +136,101 @@ export class WebhookDeliveriesListPresenter {
157136
},
158137
};
159138
}
139+
140+
/**
141+
* Count deliveries newer than `since` that match the same filters the list is showing, for the
142+
* live "N new deliveries" badge. Applies the same filter resolution as {@link call} so the badge
143+
* never counts events the filtered list would exclude. `webhookEndpointId` scopes to a single
144+
* endpoint (the per-webhook page); `webhooks` is the cross-endpoint handler filter.
145+
*/
146+
async countNewDeliveries({
147+
organizationId,
148+
projectId,
149+
environmentId,
150+
webhookEndpointId,
151+
webhooks,
152+
statuses,
153+
deliveryId,
154+
runId,
155+
isTest,
156+
since,
157+
to,
158+
}: {
159+
organizationId: string;
160+
projectId: string;
161+
environmentId: string;
162+
webhookEndpointId?: string;
163+
webhooks?: string[];
164+
statuses?: WebhookDeliveryStatus[];
165+
deliveryId?: string;
166+
runId?: string;
167+
isTest?: boolean;
168+
since: number;
169+
to?: number;
170+
}): Promise<number> {
171+
if (to !== undefined && to <= since) return 0;
172+
173+
const { webhookEndpointIds, internalRunId } = await this.#resolveFilterScope(
174+
environmentId,
175+
webhooks,
176+
runId
177+
);
178+
179+
const repository = webhookDeliveriesRepository({
180+
clickhouse: this.clickhouse,
181+
prisma: webhookReplica,
182+
});
183+
184+
const { deliveryIds } = await repository.listDeliveryIds({
185+
organizationId,
186+
projectId,
187+
environmentId,
188+
webhookEndpointId,
189+
webhookEndpointIds,
190+
deliveryId,
191+
runId: internalRunId,
192+
statuses,
193+
isTest,
194+
from: since + 1,
195+
to,
196+
page: { size: 100 },
197+
});
198+
199+
return deliveryIds.length;
200+
}
201+
202+
/**
203+
* Resolve the handler-slug webhook filter to endpoint ids and the friendly runId to the internal
204+
* id. A non-empty filter that matches nothing resolves to a sentinel that can never match, so the
205+
* filter returns nothing rather than being dropped.
206+
*/
207+
async #resolveFilterScope(
208+
environmentId: string,
209+
webhooks?: string[],
210+
runId?: string
211+
): Promise<{ webhookEndpointIds?: string[]; internalRunId?: string }> {
212+
let webhookEndpointIds: string[] | undefined;
213+
if (webhooks && webhooks.length > 0) {
214+
const endpoints = await webhookReplica.webhookEndpoint.findMany({
215+
where: {
216+
runtimeEnvironmentId: environmentId,
217+
handlerWebhookId: { in: boundedIn(webhooks) },
218+
},
219+
select: { id: true },
220+
});
221+
webhookEndpointIds = endpoints.length > 0 ? endpoints.map((e) => e.id) : ["__none__"];
222+
}
223+
224+
let internalRunId: string | undefined;
225+
if (runId) {
226+
const run = await runStore.findRun(
227+
{ friendlyId: runId },
228+
{ select: { id: true } },
229+
this.replica
230+
);
231+
internalRunId = run?.id ?? "__none__";
232+
}
233+
234+
return { webhookEndpointIds, internalRunId };
235+
}
160236
}

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.webhooks.deliveries.live.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
22
import { typedjson } from "remix-typedjson";
33
import { z } from "zod";
4+
import { type WebhookDeliveryStatus } from "@trigger.dev/database";
45
import { $replica, webhookReplica } from "~/db.server";
6+
import { WebhookDeliveriesListPresenter } from "~/presenters/v3/WebhookDeliveriesListPresenter.server";
57
import { resolveDeliveryRunTargets } from "~/presenters/v3/WebhookDetailPresenter.server";
68
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
79
import { loadProjectEnvironmentFromRequest } from "~/services/loadProjectEnvironmentFromRequest.server";
@@ -36,6 +38,38 @@ const SearchParamsSchema = z.object({
3638
to: z.coerce.number().optional(),
3739
});
3840

41+
const KNOWN_DELIVERY_STATUSES: readonly WebhookDeliveryStatus[] = [
42+
"PENDING",
43+
"PROCESSING",
44+
"SUCCEEDED",
45+
"FAILED",
46+
"FILTERED",
47+
];
48+
49+
/**
50+
* Parse the deliveries-list filters so the new-deliveries count applies the same ones the list is
51+
* showing (its badge must never count events the filtered list would exclude). Mirrors the top-level
52+
* list's param parsing: repeated or CSV `statuses`, repeated `webhooks`, `deliveryId`, `runId`, `test`.
53+
*/
54+
function parseListFilters(searchParams: URLSearchParams) {
55+
const statusValues = searchParams
56+
.getAll("statuses")
57+
.flatMap((value) => value.split(","))
58+
.map((value) => value.trim())
59+
.filter((value): value is WebhookDeliveryStatus =>
60+
KNOWN_DELIVERY_STATUSES.includes(value as WebhookDeliveryStatus)
61+
);
62+
const webhooks = searchParams.getAll("webhooks").filter((value) => value.length > 0);
63+
const testParam = searchParams.get("test");
64+
return {
65+
statuses: statusValues.length > 0 ? statusValues : undefined,
66+
webhooks: webhooks.length > 0 ? webhooks : undefined,
67+
deliveryId: searchParams.get("deliveryId") ?? undefined,
68+
runId: searchParams.get("runId") ?? undefined,
69+
isTest: testParam === "only" ? true : testParam === "hide" ? false : undefined,
70+
};
71+
}
72+
3973
export type LiveDeliveryFields = {
4074
friendlyId: string;
4175
status: ListedWebhookDelivery["status"];
@@ -114,19 +148,20 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
114148
: Promise.resolve([]),
115149
newDeliveriesSince !== undefined
116150
? (async () => {
117-
if (to !== undefined && to <= newDeliveriesSince) {
118-
return { count: 0, since: newDeliveriesSince };
119-
}
120-
const { deliveryIds: newIds } = await repository.listDeliveryIds({
151+
const filters = parseListFilters(url.searchParams);
152+
const count = await new WebhookDeliveriesListPresenter(
153+
$replica,
154+
clickhouse
155+
).countNewDeliveries({
121156
organizationId: project.organizationId,
122157
projectId: project.id,
123158
environmentId: environment.id,
124159
webhookEndpointId,
125-
from: newDeliveriesSince + 1,
160+
...filters,
161+
since: newDeliveriesSince,
126162
to,
127-
page: { size: 100 },
128163
});
129-
return { count: newIds.length, since: newDeliveriesSince };
164+
return { count, since: newDeliveriesSince };
130165
})()
131166
: Promise.resolve(undefined),
132167
]);

0 commit comments

Comments
 (0)