Skip to content

Commit

Permalink
cleanup in preparation for public release
Browse files Browse the repository at this point in the history
  • Loading branch information
nonrational committed Oct 23, 2024
1 parent 9f203a2 commit efc3538
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 100 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/dmz-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: DMZ Deploy
name: run-dmz

on:
push:
Expand All @@ -23,8 +23,8 @@ jobs:
with:
deno-version: v2.x

- name: Set "As Of" Date
run: deno scripts/cache-as-of.ts | tee -a .env
- name: Add CANIUSE_AS_OF_EPOCH to .env
run: deno scripts/print-as-of-env.ts | tee -a .env

- name: Build step
run: 'deno task build'
Expand Down
6 changes: 3 additions & 3 deletions islands/ua_input_submit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ const UaInputSubmit = ({ ok, value }: { ok: boolean; value?: string }) => {
return (
<div class='relative flex h-10 w-full min-w-[200px] max-w-4xl'>
<button
class='!absolute right-1 top-1 z-10 select-none rounded bg-slate-300 py-2 px-4 text-center align-middle font-sans text-xs font-bold uppercase text-white shadow-md shadow-slate-300/20 transition-all hover:shadow-lg hover:shadow-slate-300/40 focus:opacity-[0.85] focus:shadow-none active:opacity-[0.85] active:shadow-none peer-placeholder-shown:pointer-events-none peer-placeholder-shown:bg-blue-gray-500 peer-placeholder-shown:opacity-50 peer-placeholder-shown:shadow-none'
class='!absolute right-1 top-1 z-10 select-none rounded bg-slate-300 py-2 px-4 text-center align-middle font-sans text-xs font-bold uppercase text-white shadow-md shadow-slate-300/20 hover:shadow-lg hover:shadow-slate-300/40 focus:opacity-[0.85] focus:shadow-none active:opacity-[0.85] active:shadow-none peer-placeholder-shown:pointer-events-none peer-placeholder-shown:bg-blue-gray-500 peer-placeholder-shown:opacity-50 peer-placeholder-shown:shadow-none'
type='submit'
>
<span style={{ textShadow: '0 0 2px white' }}>🔍</span>
</button>
<input
class='peer h-full w-full rounded-[7px] border border-blue-gray-200 bg-transparent px-3 py-2.5 pr-20 font-sans text-sm font-normal text-blue-gray-700 outline outline-0 transition-all placeholder-shown:border placeholder-shown:border-blue-gray-200 placeholder-shown:border-t-blue-gray-200 focus:border-t-transparent focus:outline-0 disabled:border-0 disabled:bg-blue-gray-50'
class='peer h-full w-full rounded-[7px] border border-blue-gray-200 bg-transparent px-3 py-2.5 pr-20 font-sans text-sm font-normal text-blue-gray-700 outline outline-0 placeholder-shown:border placeholder-shown:border-blue-gray-200 placeholder-shown:border-t-blue-gray-200 focus:border-t-transparent focus:outline-0 disabled:border-0 disabled:bg-blue-gray-50'
name='ua'
value={value}
style={{ width: '100%', wordBreak: 'break-word', resize: 'both', borderColor: ok ? null : '#b91c1c' }}
onClick={(e) => e.currentTarget.select()}
/>
<label class="before:content[' '] after:content[' '] pointer-events-none absolute left-0 -top-1.5 flex h-full w-full select-none text-[11px] font-normal leading-tight text-blue-gray-400 transition-all before:pointer-events-none before:mt-[6.5px] before:mr-1 before:box-border before:block before:h-1.5 before:w-2.5 before:rounded-tl-md before:border-t before:border-l before:border-blue-gray-200 before:transition-all after:pointer-events-none after:mt-[6.5px] after:ml-1 after:box-border after:block after:h-1.5 after:w-2.5 after:flex-grow after:rounded-tr-md after:border-t after:border-r after:border-blue-gray-200 after:transition-all peer-placeholder-shown:text-sm peer-placeholder-shown:leading-[3.75] peer-placeholder-shown:text-blue-gray-500 peer-placeholder-shown:before:border-transparent peer-placeholder-shown:after:border-transparent peer-disabled:text-transparent peer-disabled:before:border-transparent peer-disabled:after:border-transparent peer-disabled:peer-placeholder-shown:text-blue-gray-500">
<label class="before:content[' '] after:content[' '] pointer-events-none absolute left-0 -top-1.5 flex h-full w-full select-none text-[11px] font-normal leading-tight text-blue-gray-400 before:pointer-events-none before:mt-[6.5px] before:mr-1 before:box-border before:block before:h-1.5 before:w-2.5 before:rounded-tl-md before:border-t before:border-l before:border-blue-gray-200 before:transition-all after:pointer-events-none after:mt-[6.5px] after:ml-1 after:box-border after:block after:h-1.5 after:w-2.5 after:flex-grow after:rounded-tr-md after:border-t after:border-r after:border-blue-gray-200 after:transition-all peer-placeholder-shown:text-sm peer-placeholder-shown:leading-[3.75] peer-placeholder-shown:text-blue-gray-500 peer-placeholder-shown:before:border-transparent peer-placeholder-shown:after:border-transparent peer-disabled:text-transparent peer-disabled:before:border-transparent peer-disabled:after:border-transparent peer-disabled:peer-placeholder-shown:text-blue-gray-500">
User Agent
</label>
</div>
Expand Down
57 changes: 31 additions & 26 deletions lib/agent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import caniuse_lite, { type CanIUseAgentName, getCanIUseLastUpdatedAt, type Version } from './caniuse-lite.ts'
import caniuse_lite, { type AgentLiteStats, type CanIUseAgentName, getCanIUseLastUpdatedAt, type Version } from './caniuse-lite.ts'

import { UserAgent } from '$std/http/user_agent.ts'
import { debug, epochToDate } from './utils.ts'
Expand Down Expand Up @@ -69,26 +69,19 @@ const getFixedDeviceType = (ua: UserAgent): 'desktop' | 'mobile' | 'tablet' | un
return undefined
}

const getCurrentVersion = (agentName: CanIUseAgentName): Version | undefined => {
const agent = caniuse_lite.agents[agentName]

if (agent === undefined) {
console.warn(`agent.getCurrentVersion: caniuse-lite data not found for '${agentName}'`)
return undefined
}

return Object.entries(caniuse_lite.agents[agentName].release_date)
const getCurrentVersion = (stats: AgentLiteStats): Version | undefined => {
return Object.entries(stats.release_date)
.filter(([_, date]) => date !== null)
.sort(([, dateA], [, dateB]) => dateB - dateA)[0][0]
}

/*
* Get get the ranking of a version in
*/
const getVersionRank = (agentName: CanIUseAgentName, version?: AgentVersion): Rank | undefined => {
const getVersionRank = (stats: AgentLiteStats, version?: AgentVersion): Rank | undefined => {
if (!version) return undefined

const ranked = Object.entries(caniuse_lite.agents[agentName].usage_global)
const ranked = Object.entries(stats.usage_global)
.filter(([_, usage]) => usage && usage > 0)
.sort(([, usageA], [, usageB]) => usageB - usageA)

Expand All @@ -97,17 +90,17 @@ const getVersionRank = (agentName: CanIUseAgentName, version?: AgentVersion): Ra
return typeof value !== 'number' || value === -1 ? undefined : { place: value + 1, version: needle, outOf: ranked.length }
}

const getVersionReleaseDate = (agentName: CanIUseAgentName, version?: AgentVersion): ReleaseDate | undefined => {
const getVersionReleaseDate = (stats: AgentLiteStats, version?: AgentVersion): ReleaseDate | undefined => {
if (!version) return undefined
// TODO(@nonrational): Provide support for checking whether a range matches the given version.
const { value, needle } = findMostSpecificVersionValue((tv: string) => caniuse_lite.agents[agentName].release_date[tv], version)
const { value, needle } = findMostSpecificVersionValue((tv: string) => stats.release_date[tv], version)
return typeof value === 'number' ? { date: epochToDate(value), version: needle } : undefined
}

const getGlobalVersionUsage = (agentName: CanIUseAgentName, version?: AgentVersion): Usage | undefined => {
const getGlobalVersionUsage = (stats: AgentLiteStats, version?: AgentVersion): Usage | undefined => {
if (!version) return undefined
// TODO(@nonrational): Provide support for checking whether a range matches the given version.
const { value, needle } = findMostSpecificVersionValue((tv: string) => caniuse_lite.agents[agentName].usage_global[tv], version)
const { value, needle } = findMostSpecificVersionValue((tv: string) => stats.usage_global[tv], version)
return typeof value === 'number' ? { percent: value, version: needle } : undefined
}

Expand All @@ -120,25 +113,37 @@ export const getAgentReleaseInfo = (ua: string): Agent => {
if (name === undefined || name === 'Unknown') return { ok: false }

const caniuseAgentName = CANIUSE_AGENT_NAMES[name]

const manifestVersion = version ? AgVer.parse(version) : undefined

const envAsOf = Deno.env.get('AS_OF_EPOCH')
// On deploy, we cache the most recently released browser date to prevent the need to recalculate it on every request
// If the env doesn't have a value set, we'll fetch it at runtime.
const envAsOf = Deno.env.get('CANIUSE_AS_OF_EPOCH')
const asOfEpoch = envAsOf ? parseInt(envAsOf) : getCanIUseLastUpdatedAt()

const result: Agent = {
const thinResponse = {
ok: true,
// TODO(@nonrational): This should probably live inside each object, so we set separate expectations for release date lookups, usage, and rank can be.
asOf: new Date(asOfEpoch * 1000),
asOf: epochToDate(asOfEpoch),
userAgent,
name,
version: manifestVersion,
userAgent,
deviceType: getFixedDeviceType(userAgent),
familyName: getFamilyName(name),
currentVersion: getCurrentVersion(caniuseAgentName),
releaseDate: getVersionReleaseDate(caniuseAgentName, manifestVersion),
usage: getGlobalVersionUsage(caniuseAgentName, manifestVersion),
rank: getVersionRank(caniuseAgentName, manifestVersion),
deviceType: getFixedDeviceType(userAgent),
}

const stats = caniuse_lite.agents[caniuseAgentName || '']

if (stats === undefined) {
console.warn(`getAgentReleaseInfo: caniuse-lite data not found for '${name}'`)
return thinResponse
}

const result: Agent = {
...thinResponse,
currentVersion: getCurrentVersion(stats),
releaseDate: getVersionReleaseDate(stats, manifestVersion),
usage: getGlobalVersionUsage(stats, manifestVersion),
rank: getVersionRank(stats, manifestVersion),
}

debug('getAgentReleaseInfo result', result)
Expand Down
4 changes: 2 additions & 2 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const andJoin = (arr: string[]) => {
return arr.slice(0, -1).join(', ') + ', and ' + arr.slice(-1)[0]
}

export const humanizedTimeSince = (on: Date | undefined | null): string | null => {
export const humanizeDurationSince = (on: Date | undefined | null): string | null => {
if (!on) return null

const now = new Date()
Expand All @@ -45,7 +45,7 @@ export const humanizedTimeSince = (on: Date | undefined | null): string | null =
if (adjustedYears > 0) parts.push(`${adjustedYears} year${adjustedYears > 1 ? 's' : ''}`)
if (adjustedMonths > 0) parts.push(`${adjustedMonths} month${adjustedMonths > 1 ? 's' : ''}`)

return andJoin(parts) + ' ago'
return andJoin(parts)
}

export const randInterjection = () => {
Expand Down
22 changes: 9 additions & 13 deletions routes/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,19 @@ export default function App({ Component }: PageProps) {
<link rel='stylesheet' href='/styles.css' />

<link rel='preconnect' href='https://fonts.googleapis.com' />
<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin='true' />
<link rel='stylesheet' href='https://fonts.googleapis.com/css2?family=Merienda:[email protected]&display=swap' />
<link rel='preconnect' href='https://fonts.gstatic.com' />
<link
href='https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'
rel='stylesheet'
/>
</head>
<body>
<body class='inter-regular'>
<Component />
</body>
<footer className='merienda-attrib flex flex-row justify-around'>
<span>
Made with <span style={{ filter: 'grayscale(50%)' }}>❤️</span> and <span style={{ filter: 'grayscale(50%)' }}>🦕</span> .
</span>
<span>
Brought to you by{' '}
<span>
<em>
<u>your name here</u>
</em>!
</span>
<span className='text-sm'>
Built with <span>☕️</span> and <span>🦕</span> by{' '}
<a href='https://github.com/nonrational/user-agent.info' target='_blank' rel='noopener noreferrer'>@nonrational</a>
</span>
</footer>
</html>
Expand Down
108 changes: 57 additions & 51 deletions routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { FunctionalComponent } from 'preact'
import type { Handlers, PageProps } from '$fresh/server.ts'

import { type Agent, getAgentReleaseInfo } from '../lib/agent.ts'
import { formatDateYearMonth, humanizedTimeSince, randInterjection, toOrdinal } from '../lib/utils.ts'
import { formatDateYearMonth, humanizeDurationSince, randInterjection, toOrdinal } from '../lib/utils.ts'
import { getFamilyName, getGlobalUsageStats, getNorthAmericaUsageStats } from '../lib/family.ts'
import AgVer from '../lib/agent_version.ts'
import UaInputSubmit from '../islands/ua_input_submit.tsx'
Expand All @@ -27,34 +27,28 @@ export const handler: Handlers = {
},
}

const AgentAck = ({ ok, version, userAgent, name, source, deviceType }: RenderData) => {
const AgentIdentification = ({ ok, version, userAgent, name, source }: RenderData) => {
return (
<p>
{!ok && (
<>
Hmm. That agent must still be undercover. <a href='/' class='underline'>Start over</a>.
{
/* If you've seen this agent in the wild,{' '}
<a href='#' class='line-through' title='Coming soon!'>open an issue</a>. */
}
</>
)}

{ok && source === 'query' && <>Ok! That looks to be</>}
{ok && source === 'query' && <>That's</>}
{ok && source === 'header' && <>Looks like you're using</>}
{ok && (
<>
{' '}
<strong>{name} {AgVer.tryFormat(version)}</strong> running <strong>{userAgent?.os.name}</strong> on <strong>{deviceType}</strong>.
{' '}
{randInterjection()}.
<strong>{name} {AgVer.tryFormat(version)}</strong> on <strong>{userAgent?.os.name}</strong>. {randInterjection()}.
</>
)}
</p>
)
}

const ReleaseDescription = ({ name, releaseDate }: RenderData) => {
const AgentReleaseAge = ({ name, releaseDate }: RenderData) => {
if (!releaseDate) return null

const { date, version } = releaseDate
Expand All @@ -63,36 +57,50 @@ const ReleaseDescription = ({ name, releaseDate }: RenderData) => {
<p>
{name} {version}
{date
? ` was released ${humanizedTimeSince(date)}, in ${formatDateYearMonth(date)}.`
? ` was released in ${formatDateYearMonth(date)}; it's ${humanizeDurationSince(date)} old.`
: ` hasn't officially been released yet. Far out.`}
</p>
)
}

const UsageStats = ({ deviceType, userAgent, version, name, usage, currentVersion }: RenderData) => {
if (!userAgent) return null
const AgentReleaseUsage = ({ userAgent, name, usage, rank, currentVersion }: RenderData) => {
if (!userAgent || !usage) return null

const hasSomeUsage = usage.percent > 0.000
const major = userAgent?.browser.major
const globalUsage = getGlobalUsageStats(userAgent.browser, userAgent.device)
const americasUsage = getNorthAmericaUsageStats(userAgent.browser, userAgent.device)

const preferMajor = !usage || usage.percent < 0.001
const bestVersion = preferMajor ? major : usage.version

const showVersionStats = currentVersion === major || usage
if (!hasSomeUsage) {
return <p>{name} {usage.version} has{usage.percent > 0 ? ' nearly' : ''} 0% usage worldwide.</p>
}

return (
<p>
{showVersionStats && name && version && <>{name} {bestVersion}</>}
{currentVersion === major && <>{' '}is the most recent{preferMajor && ' major'} release{usage ? ' and' : '.'}</>}
{usage && (
{name} {usage?.version}
{currentVersion === major && <>{' '}is the most recent major release{usage ? ' and' : '.'}</>} represents{' '}
<strong>{usage.percent.toFixed(3)}%</strong> of global browser traffic.
{rank && (
<>
{' '}represents <strong>{usage.percent.toFixed(3)}%</strong> of global browser traffic.
{' '}
It's currently the {rank.place > 1 && toOrdinal(rank.place)} most popular version of {name} (out of {rank.outOf}{' '}
active versions) worldwide.
</>
)}
{showVersionStats && ' '}
</p>
)
}

const AgentUsage = ({ name, deviceType, userAgent }: RenderData) => {
if (!userAgent) return null

const globalUsage = getGlobalUsageStats(userAgent.browser, userAgent.device)
const americasUsage = getNorthAmericaUsageStats(userAgent.browser, userAgent.device)

if (globalUsage < 0.01 && americasUsage < 0.01) return null

return (
<p>
{name && deviceType && <>{getFamilyName(name)} on {deviceType} is used by{' '}</>}
<strong>{americasUsage.toFixed(3)}</strong>% of North America and <strong>{globalUsage.toFixed(2)}%</strong> of the world.
<strong>{americasUsage.toFixed(1)}</strong>% of North America and <strong>{globalUsage.toFixed(1)}%</strong> of the world.
</p>
)
}
Expand All @@ -104,7 +112,7 @@ const FormattedUserAgent = ({ userAgent }: { userAgent?: UserAgent }) => {
const uaParts = userAgent.ua.replace(/([A-z]+\/[0-9.]+)/g, '¶$1').split('¶')

return (
<code class='block my-4 text-sm break-word text-center'>
<code class='text-sm text-center'>
{uaParts.map((part, index) => <div key={`ua-${index}`}>{part}</div>)}
</code>
)
Expand All @@ -116,35 +124,33 @@ const Home: FunctionalComponent<HomeProps> = ({ data }: PageProps) => {
const inputValue = ok ? userAgent?.ua : ua

return (
<div class='container mx-auto flex flex-col items-center justify-center max-w-xs sm:max-w-sm md:max-w-lg lg:max-w-4xl'>
<div class='container mx-auto flex flex-col items-center justify-center p-2'>
<div class='font-bold mt-8 text-2xl md:text-4xl flex flex-row items-center justify-center gap-4'>
<div>🕵️</div>
<div>user-agent.info</div>
</div>

<form method='GET' style={{ width: '100%', display: 'block', margin: '24px 0' }}>
<UaInputSubmit ok={ok} value={inputValue} />
</form>

<FormattedUserAgent userAgent={userAgent} />

<div class='p-8'>
<AgentAck {...data} />

{ok &&
(
<>
<ReleaseDescription {...data} />
<UsageStats {...data} />
{data.rank && (
<p>
Version {data.rank.version} is currently the {data.rank.place > 1 && toOrdinal(data.rank.place)} most popular version of
{' '}
{data.name} (out of {data.rank.outOf} active versions) worldwide.
</p>
)}
</>
)}
<div class='my-8 w-full lg:max-w-4xl'>
<form method='GET' class='w-full'>
<UaInputSubmit ok={ok} value={inputValue} />
</form>
</div>

<div class='text-xl md:text-2xl xl:text-4xl py-4 sm:px-2'>
<AgentIdentification {...data} />
</div>

{ok &&
(
<div class='text-lg md:max-w-2xl sm:px-2 md:px-4'>
<AgentReleaseAge {...data} />
<AgentReleaseUsage {...data} />
<AgentUsage {...data} />
</div>
)}

<div class='mb-16'>
<FormattedUserAgent userAgent={userAgent} />
</div>
</div>
)
Expand Down
Loading

0 comments on commit efc3538

Please sign in to comment.