Skip to content

Commit 556af51

Browse files
committed
Drop fuzzy name matching from the preset icon matcher
1 parent b569b56 commit 556af51

2 files changed

Lines changed: 57 additions & 183 deletions

File tree

packages/react/src/components/integration-favicon.test.tsx

Lines changed: 50 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -92,63 +92,11 @@ describe("IntegrationFavicon", () => {
9292
).toBe("https://example.com/sheets.svg");
9393
});
9494

95-
it("finds preset icons from display names with suffixes", () => {
96-
expect(
97-
integrationPresetIconUrl(
98-
{
99-
id: "google_search_console_api",
100-
kind: "googleDiscovery",
101-
name: "Google Search Console API",
102-
},
103-
[
104-
{
105-
key: "google",
106-
label: "Google",
107-
add: () => null,
108-
edit: () => null,
109-
presets: [
110-
{
111-
id: "google-search-console",
112-
name: "Google Search Console",
113-
summary: "Search performance.",
114-
icon: "https://example.com/google.svg",
115-
},
116-
],
117-
},
118-
],
119-
),
120-
).toBe("https://example.com/google.svg");
121-
});
122-
123-
it("finds preset icons from an integration id when the URL is missing", () => {
124-
expect(
125-
integrationPresetIconUrl(
126-
{
127-
id: "sentry",
128-
kind: "mcp",
129-
name: "Sentry MCP",
130-
},
131-
[
132-
{
133-
key: "mcp",
134-
label: "MCP",
135-
add: () => null,
136-
edit: () => null,
137-
presets: [
138-
{
139-
id: "sentry",
140-
name: "Sentry",
141-
summary: "Errors.",
142-
icon: "https://example.com/sentry.png",
143-
},
144-
],
145-
},
146-
],
147-
),
148-
).toBe("https://example.com/sentry.png");
149-
});
150-
151-
it("matches migrated MCP slugs with host/suffix noise", () => {
95+
it("does not fuzzy-match preset icons from names or slugs", () => {
96+
// Name/slug token matching is gone: without an exact defaultSlug or URL
97+
// match, the preset matcher declines and the cascade resolves through the
98+
// integrations.sh favicon instead, which is derived from the integration's
99+
// own domain and therefore cannot render the wrong brand.
152100
const presets = [
153101
{
154102
key: "mcp",
@@ -157,63 +105,25 @@ describe("IntegrationFavicon", () => {
157105
edit: () => null,
158106
presets: [
159107
{
160-
id: "posthog",
161-
name: "PostHog",
162-
summary: "Analytics.",
163-
icon: "https://example.com/posthog.png",
164-
},
165-
{
166-
id: "linear",
167-
name: "Linear",
168-
summary: "Issues.",
169-
icon: "https://example.com/linear.png",
170-
},
171-
{
172-
id: "planetscale",
173-
name: "PlanetScale",
174-
summary: "Databases.",
175-
icon: "https://example.com/pscale.png",
108+
id: "sentry",
109+
name: "Sentry",
110+
summary: "Errors.",
111+
icon: "https://example.com/sentry.png",
176112
},
177113
],
178114
},
179115
];
180116

181117
expect(
182-
integrationPresetIconUrl(
183-
{ id: "mcp_posthog_com", kind: "mcp", name: "mcp.posthog.com" },
184-
presets,
185-
),
186-
).toBe("https://example.com/posthog.png");
187-
expect(
188-
integrationPresetIconUrl(
189-
{ id: "mcp_linear_app", kind: "mcp", name: "mcp.linear.app" },
190-
presets,
191-
),
192-
).toBe("https://example.com/linear.png");
193-
expect(
194-
integrationPresetIconUrl({ id: "pscale_mcp", kind: "mcp", name: "Pscale MCP" }, presets),
195-
).toBe("https://example.com/pscale.png");
196-
});
118+
integrationPresetIconUrl({ id: "sentry", kind: "mcp", name: "Sentry MCP" }, presets),
119+
).toBeNull();
197120

198-
it("matches migrated OpenAPI slugs with API/REST suffixes", () => {
199-
expect(
200-
integrationPresetIconUrl({ id: "stripe_api", kind: "openapi", name: "Stripe API" }, [
201-
{
202-
key: "openapi",
203-
label: "OpenAPI",
204-
add: () => null,
205-
edit: () => null,
206-
presets: [
207-
{
208-
id: "stripe",
209-
name: "Stripe",
210-
summary: "Payments.",
211-
icon: "https://example.com/stripe.png",
212-
},
213-
],
214-
},
215-
]),
216-
).toBe("https://example.com/stripe.png");
121+
// Migrated host-shaped integrations resolve through the inferred URL:
122+
const inferred = integrationInferredUrl({ id: "mcp_posthog_com", name: "mcp.posthog.com" });
123+
expect(inferred).toBe("https://mcp.posthog.com");
124+
expect(integrationFaviconSrc({ url: inferred ?? undefined, size: 16 })).toBe(
125+
"https://integrations.sh/logo/posthog.com?sz=32",
126+
);
217127
});
218128

219129
it("prefers exact catalog defaultSlug icons for OpenAPI provider services", () => {
@@ -238,7 +148,25 @@ describe("IntegrationFavicon", () => {
238148
).toBe("https://example.com/gmail.png");
239149
});
240150

241-
it("does not split generic words into brand matches", () => {
151+
it("matches presets on exact URL equality, not names", () => {
152+
const presets = [
153+
{
154+
key: "openapi",
155+
label: "OpenAPI",
156+
add: () => null,
157+
edit: () => null,
158+
presets: [
159+
{
160+
id: "spotify",
161+
name: "Spotify",
162+
summary: "Music.",
163+
url: "https://api.spotify.com/v1",
164+
icon: "https://example.com/spotify.png",
165+
},
166+
],
167+
},
168+
];
169+
242170
expect(
243171
integrationPresetIconUrl(
244172
{
@@ -247,30 +175,22 @@ describe("IntegrationFavicon", () => {
247175
name: "Spotify Web API",
248176
url: "https://api.spotify.com/v1",
249177
},
250-
[
251-
{
252-
key: "openapi",
253-
label: "OpenAPI",
254-
add: () => null,
255-
edit: () => null,
256-
presets: [
257-
{
258-
id: "exa-websets",
259-
name: "Exa Websets",
260-
summary: "Web data.",
261-
icon: "https://example.com/exa.png",
262-
},
263-
{
264-
id: "spotify",
265-
name: "Spotify",
266-
summary: "Music.",
267-
icon: "https://example.com/spotify.png",
268-
},
269-
],
270-
},
271-
],
178+
presets,
272179
),
273180
).toBe("https://example.com/spotify.png");
181+
182+
// Same name, different URL: no match — the URL-derived favicon takes over.
183+
expect(
184+
integrationPresetIconUrl(
185+
{
186+
id: "spotify_web_api",
187+
kind: "openapi",
188+
name: "Spotify Web API",
189+
url: "https://api.spotify.com/v2",
190+
},
191+
presets,
192+
),
193+
).toBeNull();
274194
});
275195

276196
it("does not match a different brand sharing a word fragment (ClickHouse Cloud vs Cloudflare)", () => {

packages/react/src/components/integration-favicon.tsx

Lines changed: 7 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -76,46 +76,6 @@ const googleApiServiceFromUrl = (url: string | undefined): string | null => {
7676
return null;
7777
};
7878

79-
const normalizeToken = (value: string | undefined): string =>
80-
value?.toLowerCase().replace(/[^a-z0-9]+/g, "") ?? "";
81-
82-
const tokenVariants = (value: string | undefined): readonly string[] => {
83-
const tokens = new Set<string>();
84-
const normalized = normalizeToken(value);
85-
if (normalized.length > 0) tokens.add(normalized);
86-
87-
const parts =
88-
value
89-
?.toLowerCase()
90-
.split(/[^a-z0-9]+/g)
91-
.filter(Boolean) ?? [];
92-
const noise = new Set([
93-
"api",
94-
"mcp",
95-
"openapi",
96-
"rest",
97-
"graphql",
98-
"web",
99-
"com",
100-
"net",
101-
"org",
102-
"dev",
103-
]);
104-
const cleaned = parts.filter((part) => !noise.has(part));
105-
const cleanedToken = cleaned.join("");
106-
if (cleanedToken.length > 0) tokens.add(cleanedToken);
107-
const aliases: Record<string, string> = {
108-
pscale: "planetscale",
109-
};
110-
for (const part of cleaned) {
111-
tokens.add(part);
112-
const alias = aliases[part];
113-
if (alias) tokens.add(alias);
114-
}
115-
116-
return [...tokens];
117-
};
118-
11979
export function integrationInferredUrl(integration: {
12080
readonly id: string;
12181
readonly name?: string;
@@ -129,13 +89,12 @@ export function integrationInferredUrl(integration: {
12989
return null;
13090
}
13191

132-
// Exact equality only: substring containment matched unrelated brands that
133-
// merely share a fragment ("ClickHouse Cloud" ⊂ "Cloudflare" via "cloud"). A
134-
// missed match is recoverable (the cascade falls through to the domain-derived
135-
// integrations.sh favicon); a wrong-brand icon is not.
136-
const tokenMatches = (integrationValue: string, presetValue: string): boolean =>
137-
presetValue.length > 0 && integrationValue === presetValue;
138-
92+
// Exact identity signals only — preset defaultSlug, normalized URL equality,
93+
// or the same Google discovery service. Fuzzy name/slug token matching used to
94+
// live here and matched unrelated brands sharing a word fragment ("ClickHouse
95+
// Cloud" rendered Cloudflare's logo via "cloud"). A missed match is recoverable
96+
// (the cascade falls through to the domain-derived integrations.sh favicon,
97+
// which is always the right brand); a wrong-brand icon is not.
13998
export function integrationPresetIconUrl(
14099
integration: {
141100
readonly id: string;
@@ -153,18 +112,13 @@ export function integrationPresetIconUrl(
153112

154113
const integrationUrl = normalizeUrl(integration.url);
155114
const integrationGoogleService = googleApiServiceFromUrl(integration.url);
156-
const integrationTokens = [...tokenVariants(integration.id), ...tokenVariants(integration.name)];
157115

158116
const preset = presets.find((p) => {
159117
const presetUrl = normalizeUrl(p.url);
160118
const presetGoogleService = googleApiServiceFromUrl(p.url);
161-
const presetTokens = [...tokenVariants(p.id), ...tokenVariants(p.name)];
162119
return (
163120
(integrationUrl !== null && presetUrl === integrationUrl) ||
164-
(integrationGoogleService !== null && presetGoogleService === integrationGoogleService) ||
165-
integrationTokens.some((integrationToken) =>
166-
presetTokens.some((presetToken) => tokenMatches(integrationToken, presetToken)),
167-
)
121+
(integrationGoogleService !== null && presetGoogleService === integrationGoogleService)
168122
);
169123
});
170124

0 commit comments

Comments
 (0)