Skip to content

Commit 7367118

Browse files
committed
fix(webapp): cap how long an empty customer card is cached, add release note
Omitting timeToLiveSeconds on a no-data card falls back to the TTL configured for that card in Plain, so a customer who becomes resolvable — an external id gets set, or someone signs up with that address — would keep showing an empty card for however long that default is. Set explicitly to 60s. An external id that no longer resolves is now logged. The email match keeps the card useful, but a stale link we wrote ourselves should not stay invisible.
1 parent b41af49 commit 7367118

4 files changed

Lines changed: 38 additions & 5 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Fixed support threads showing no account details for some customers, so the team can see your plan, organizations and projects when you get in touch.

apps/webapp/app/routes/api.v1.plain.customer-cards.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ export async function action({ request }: ActionFunctionArgs) {
129129
byExternalId ??
130130
(email ? await prisma.user.findFirst({ where: { email }, include: userInclude }) : null);
131131

132+
// An external id we set ourselves that no longer resolves is an anomaly worth seeing, even
133+
// though the email match keeps the card useful — otherwise the stale link stays invisible.
134+
if (customer.externalId && !byExternalId) {
135+
logger.warn("Plain customer card external id did not resolve", {
136+
resolvedByEmail: !!user,
137+
});
138+
}
139+
132140
/**
133141
* Impersonation is offered only when the customer matched on `externalId` — a value we set
134142
* ourselves from `User.id`.

apps/webapp/app/utils/plainCustomerCards.test.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ describe("normalizeEmail", () => {
8181
describe("answerAllCardKeys", () => {
8282
it("adds a no-data card for every unanswered key", () => {
8383
expect(answerAllCardKeys(["a", "b"], [])).toEqual([
84-
{ key: "a", components: null },
85-
{ key: "b", components: null },
84+
{ key: "a", components: null, timeToLiveSeconds: 60 },
85+
{ key: "b", components: null, timeToLiveSeconds: 60 },
8686
]);
8787
});
8888

@@ -97,11 +97,19 @@ describe("answerAllCardKeys", () => {
9797

9898
expect(answerAllCardKeys(["a", "b", "c"], [answered])).toEqual([
9999
answered,
100-
{ key: "a", components: null },
101-
{ key: "c", components: null },
100+
{ key: "a", components: null, timeToLiveSeconds: 60 },
101+
{ key: "c", components: null, timeToLiveSeconds: 60 },
102102
]);
103103
});
104104

105+
// Omitting the TTL would fall back to the card's configured default, keeping an empty card in
106+
// Plain's cache after the customer becomes resolvable.
107+
it("caps how long an empty card is cached", () => {
108+
const [filler] = answerAllCardKeys(["a"], []);
109+
110+
expect(filler).toMatchObject({ timeToLiveSeconds: 60 });
111+
});
112+
105113
it("ignores extra cards that were not requested", () => {
106114
const extra = { key: "unrequested", components: [] };
107115

apps/webapp/app/utils/plainCustomerCards.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,17 @@ export function normalizeEmail(email: string | null | undefined): string | null
4444
return email?.toLowerCase().trim() || null;
4545
}
4646

47-
type NoDataCard = { key: string; components: null };
47+
type NoDataCard = { key: string; components: null; timeToLiveSeconds: number };
48+
49+
/**
50+
* How long Plain may cache a card we had no data for.
51+
*
52+
* Explicit rather than omitted: omitting the field falls back to the TTL configured for that card
53+
* in Plain's settings, so a customer who becomes resolvable — an external id gets set, or someone
54+
* signs up with that address — would keep showing an empty card for however long that default is.
55+
* Short enough to recover promptly, long enough not to re-ask on every glance at a thread.
56+
*/
57+
const NO_DATA_TTL_SECONDS = 60;
4858

4959
/**
5060
* Fills in a `components: null` card for every requested key that wasn't answered.
@@ -67,6 +77,7 @@ export function answerAllCardKeys<TCard extends { key: string }>(
6777
(key): NoDataCard => ({
6878
key,
6979
components: null,
80+
timeToLiveSeconds: NO_DATA_TTL_SECONDS,
7081
})
7182
),
7283
];

0 commit comments

Comments
 (0)