Skip to content

Commit 1d15062

Browse files
committed
improvement(favicons): correct the platform table from vendor docs and keep misses from flashing
GitLab Dedicated instances live on gitlab-dedicated.com (and .systems pre-production); .site is their Pages domain, kept to match gitlab.io. Adds coupacloud.com, databricks.us, databricksapps.us, and testrail.io, each a suffix both providers miss at every level; drops entries the parent walk already resolves and the self-hosted sentry label, whose name collides with other products. A host with no icon is remembered client-side for the route's miss lifetime, so a remounted icon shows its fallback without an empty frame, and the cache key moves to v3 so stale misses for newly mapped platforms clear on deploy.
1 parent ec7b31c commit 1d15062

4 files changed

Lines changed: 46 additions & 17 deletions

File tree

‎apps/sim/components/favicon.tsx‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@
22

33
import { type ReactNode, useState } from 'react'
44
import { cn } from '@sim/emcn'
5+
import { LRUCache } from 'lru-cache'
56
import { faviconSrc } from '@/lib/favicon/url'
67

8+
/**
9+
* Hosts with no icon, so an icon that mounts again renders its fallback at once instead of
10+
* asking again and showing an empty slot first. Expires with the route's caching of a miss.
11+
*/
12+
const knownMisses = new LRUCache<string, true>({ max: 500, ttl: 60 * 60 * 1000 })
13+
714
interface FaviconProps {
815
/** The site's hostname; without one the fallback renders. */
916
host: string | null | undefined
@@ -20,15 +27,18 @@ interface FaviconProps {
2027
*/
2128
export function Favicon({ host, fallback, className }: FaviconProps) {
2229
const [failedHost, setFailedHost] = useState<string | null>(null)
23-
if (!host || failedHost === host) return fallback
30+
if (!host || failedHost === host || knownMisses.has(host)) return fallback
2431
return (
2532
<img
2633
src={faviconSrc(host)}
2734
alt=''
2835
loading='lazy'
2936
decoding='async'
3037
className={cn('rounded-sm', className)}
31-
onError={() => setFailedHost(host)}
38+
onError={() => {
39+
knownMisses.set(host, true)
40+
setFailedHost(host)
41+
}}
3242
/>
3343
)
3444
}

‎apps/sim/lib/favicon/hosts.test.ts‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,13 @@ describe('faviconHostCandidates', () => {
5353
],
5454
],
5555
[
56-
'acme.lightning.force.com',
57-
['acme.lightning.force.com', 'lightning.force.com', 'force.com', 'salesforce.com'],
56+
'acme.gitlab-dedicated.com',
57+
['acme.gitlab-dedicated.com', 'gitlab-dedicated.com', 'gitlab.com'],
58+
],
59+
['acme.testrail.io', ['acme.testrail.io', 'testrail.io', 'testrail.com']],
60+
[
61+
'dbc-1.cloud.databricks.us',
62+
['dbc-1.cloud.databricks.us', 'cloud.databricks.us', 'databricks.us', 'databricks.com'],
5863
],
5964
])(
6065
'ends a platform tenant %s at the platform, after its own host and parents',
@@ -90,6 +95,14 @@ describe('faviconHostCandidates', () => {
9095
}
9196
)
9297

98+
it('adds nothing for a platform whose parent the walk already reaches', () => {
99+
expect(faviconHostCandidates('acme.my.salesforce.com')).toEqual([
100+
'acme.my.salesforce.com',
101+
'my.salesforce.com',
102+
'salesforce.com',
103+
])
104+
})
105+
93106
it('never repeats a host the walk already reaches', () => {
94107
expect(faviconHostCandidates('gitlab.gitlab.com')).toEqual(['gitlab.gitlab.com', 'gitlab.com'])
95108
})

‎apps/sim/lib/favicon/platforms.ts‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
/**
2-
* Platforms whose tenant hosts no icon provider can see — not even at the parent domain — mapped
3-
* to the platform's own site, so a tenant still shows the product it is. Generic hosting domains
4-
* (`herokuapp.com`, `azurewebsites.net`, `cloudfront.net`) are deliberately absent: they serve
5-
* other companies' apps, and the host's logo would misrepresent them.
2+
* Platforms whose tenant hosts no icon provider can see — not even at any parent the walk
3+
* reaches — mapped to the platform's own site, so a tenant still shows the product it is. A
4+
* platform whose parent already resolves (`salesforce.com`, `databricks.com`, `atlassian.net`)
5+
* needs no entry. Generic hosting domains (`herokuapp.com`, `azurewebsites.net`,
6+
* `cloudfront.net`) are deliberately absent: they serve other companies' apps, and the host's
7+
* logo would misrepresent them. GitLab Pages on Dedicated (`gitlab-dedicated.site`) maps like
8+
* `gitlab.io`, whose parent already shows GitLab's icon.
69
*/
710
const PLATFORM_SITES: ReadonlyMap<string, string> = new Map([
811
['awsapps.com', 'aws.amazon.com'],
912
['azuredatabricks.net', 'databricks.com'],
10-
['cloud.databricks.com', 'databricks.com'],
13+
['coupacloud.com', 'coupa.com'],
1114
['coupahost.com', 'coupa.com'],
15+
['databricks.us', 'databricks.com'],
1216
['databricksapps.com', 'databricks.com'],
17+
['databricksapps.us', 'databricks.com'],
18+
['gitlab-dedicated.com', 'gitlab.com'],
1319
['gitlab-dedicated.site', 'gitlab.com'],
20+
['gitlab-dedicated.systems', 'gitlab.com'],
1421
['jamacloud.com', 'jamasoftware.com'],
15-
['lightning.force.com', 'salesforce.com'],
16-
['my.salesforce.com', 'salesforce.com'],
1722
['oktapreview.com', 'okta.com'],
23+
['testrail.io', 'testrail.com'],
1824
['ultipro.com', 'ukg.com'],
1925
])
2026

@@ -25,7 +31,6 @@ const SELF_HOSTED_PRODUCTS: ReadonlyMap<string, string> = new Map([
2531
['grafana', 'grafana.com'],
2632
['jenkins', 'jenkins.io'],
2733
['jira', 'atlassian.com'],
28-
['sentry', 'sentry.io'],
2934
['testrail', 'testrail.com'],
3035
])
3136

‎apps/sim/lib/favicon/resolve.server.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type FaviconResolution =
3636

3737
const inFlight = new Map<string, Promise<FaviconResolution>>()
3838

39-
const cacheKey = (host: string) => `favicon:v2:${host}`
39+
const cacheKey = (host: string) => `favicon:v3:${host}`
4040

4141
/** A host's key holds the result of the walk that starts at that host. */
4242
async function readCache(host: string): Promise<FaviconResolution | null> {
@@ -128,10 +128,11 @@ async function walk(candidates: string[], signal: AbortSignal): Promise<FaviconR
128128
}
129129

130130
/**
131-
* The icon for a host, or its nearest parent domain's, from public icon providers — the
132-
* destination itself is never contacted, so a host behind a login still gets its platform's
133-
* icon. Results are cached per host (30 days for an icon, a day for none, minutes after an
134-
* outage), and concurrent lookups of one host in this process share a single resolution.
131+
* The icon for a host, or its nearest parent domain's, from public icon providers — Sim never
132+
* contacts the destination itself (a provider may, from its own servers), so a host behind a
133+
* login still gets its platform's icon. Results are cached per host (30 days for an icon, a day
134+
* for none or for a parent's icon chosen after a timeout, minutes after an outage), and
135+
* concurrent lookups of one host in this process share a single resolution.
135136
*/
136137
export async function resolveFavicon(host: string): Promise<FaviconResolution> {
137138
const candidates = faviconHostCandidates(host)

0 commit comments

Comments
 (0)