Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/site/public/illustrations/world-map/map.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions apps/site/src/app/api/worldmap/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { NextResponse } from "next/server";
import { airports } from "@/data/airports-code";

const ACCELERATE_ANALYTICS =
"https://accelerate-analytics-exporter.prisma-data.net/livemap-data";

function transformCoordinates(coordinates: { lat: number; lon: number }) {
let temp_lon = coordinates.lon;
if (coordinates.lon > 0 || coordinates.lon < 0) {
temp_lon = temp_lon + 180;
} else temp_lon = 180;
temp_lon = (temp_lon * 100) / 360;

let temp_lat = coordinates.lat;
if (coordinates.lat > 0 || coordinates.lat < 0) {
temp_lat = temp_lat * -1 + 90;
} else temp_lat = 90;
temp_lat = (temp_lat * 100) / 180;

return {
lon: Number(temp_lon.toFixed(2)),
lat: Number(temp_lat.toFixed(2)),
};
}

export async function GET() {
try {
const response = await fetch(ACCELERATE_ANALYTICS);

if (!response.ok) {
return NextResponse.json(
{ message: "Error fetching analytics" },
{ status: response.status },
);
}
Comment on lines +28 to +35
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

External fetch lacks a timeout, risking request hangs.

If the analytics endpoint becomes slow or unresponsive, this request will hang indefinitely. Consider adding an AbortController with a reasonable timeout (e.g., 5-10 seconds) to ensure the API route responds in a bounded time.

⏱️ Proposed timeout implementation
 export async function GET() {
   try {
-    const response = await fetch(ACCELERATE_ANALYTICS);
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 10000);
+
+    const response = await fetch(ACCELERATE_ANALYTICS, {
+      signal: controller.signal,
+    });
+    clearTimeout(timeoutId);
 
     if (!response.ok) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/api/worldmap/route.ts` around lines 28 - 35, The external
fetch to ACCELERATE_ANALYTICS in route.ts needs a timeout to avoid hanging; wrap
the fetch in an AbortController (set a 5–10s timeout via setTimeout that calls
controller.abort()), pass controller.signal to the fetch call, clear the timeout
after fetch completes, and catch AbortError to return a bounded response (e.g.,
NextResponse.json with a 504/timeout message). Update the fetch invocation that
currently uses fetch(ACCELERATE_ANALYTICS) to accept the controller.signal and
add the abort/cleanup logic around it.


const data: Array<{ pop: string; ratio: number }> = await response.json();

const cured_data = data
.filter((pop) => !!pop.pop)
.map((pop) => {
const airport = airports.find((a) => a.pop === pop.pop);
if (!airport) return null;
return {
...pop,
cured_coord: transformCoordinates(airport.coordinates),
};
})
.filter(Boolean);

const cured_airport_data = airports.map((airport) => {
const active = cured_data.find((d) => d?.pop === airport.pop);
return {
pop: airport.pop,
cured_coord: transformCoordinates(airport.coordinates),
...(active && { ratio: active.ratio }),
};
});

return NextResponse.json(cured_airport_data, {
headers: {
"Cache-Control": "s-maxage=86400, stale-while-revalidate=59",
},
});
Comment on lines +60 to +64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Cache duration of 24 hours conflicts with "live" data polling.

The s-maxage=86400 (24 hours) means CDN-cached responses won't refresh for a full day, which contradicts the client polling every 60 seconds and the "live activity" premise. If live data is important, consider reducing s-maxage to match the polling interval or slightly longer (e.g., s-maxage=60 or s-maxage=120).

🔄 Proposed cache header adjustment
     return NextResponse.json(cured_airport_data, {
       headers: {
-        "Cache-Control": "s-maxage=86400, stale-while-revalidate=59",
+        "Cache-Control": "s-maxage=60, stale-while-revalidate=30",
       },
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/api/worldmap/route.ts` around lines 60 - 64, The
Cache-Control on the NextResponse in route.ts is set to s-maxage=86400 which
prevents CDN refresh for 24h and conflicts with the client polling every ~60s;
update the headers passed to NextResponse.json (the return block) to use a much
shorter s-maxage (e.g., s-maxage=60 or s-maxage=120) and adjust
stale-while-revalidate appropriately (e.g., stale-while-revalidate=30) so the
CDN TTL matches the live polling interval.

} catch {
return NextResponse.json(
{ message: "Internal Server Error" },
{ status: 500 },
);
}
}
5 changes: 5 additions & 0 deletions apps/site/src/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
@source inline("md:hover:paused");
@source inline("hover:paused");

@keyframes pulsate {
0%, 100% { transform: scale(0.9); opacity: 0.5; }
50% { transform: scale(1); opacity: 1; }
}

@theme {
--animate-slide-down: slideDown 130s linear infinite;
--animate-slide-down-2: slideDown2 130s linear infinite;
Expand Down
105 changes: 105 additions & 0 deletions apps/site/src/app/global/_components/world-map.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"use client";

import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/cn";

type DataPoint = {
pop: string;
ratio?: number;
cured_coord: { lon: number; lat: number };
};

function Marker({ data }: { data: DataPoint }) {
const [showTooltip, setShowTooltip] = useState(false);
const isActive = Boolean(data.ratio);

return (
<>
<span
className={cn(
"absolute rounded-full",
isActive
? "bg-[#71E8DF99] border border-[#B7F4EE] shadow-[0_0_28px_0_#71E8DF99] opacity-0 animate-[pulsate_2s_ease-in-out_infinite] z-2"
: "size-2 bg-[rgba(113,128,150,1)] border border-stroke-neutral opacity-100 z-1",
)}
style={{
...(isActive && {
width: `${20 * (1 + (data.ratio || 0))}px`,
height: `${20 * (1 + (data.ratio || 0))}px`,
animationDuration: `${2 / (1 + (data.ratio || 0))}s`,
animationDelay: `${(data.ratio || 0) * 1000}ms`,
}),
...(data.cured_coord && {
left: `${data.cured_coord.lon}%`,
top: `${data.cured_coord.lat}%`,
}),
transformOrigin: "center",
}}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
/>
{showTooltip && isActive && data.pop && (
<span
className="absolute z-10 px-2 py-1 text-xs font-semibold bg-background-neutral-strong text-foreground-neutral rounded pointer-events-none whitespace-nowrap"
style={{
left: `${data.cured_coord.lon}%`,
top: `${data.cured_coord.lat}%`,
transform: "translate(-50%, -150%)",
}}
>
{data.pop}
</span>
)}
</>
);
}

export function WorldMap() {
const dataPoints = useRef<DataPoint[]>([]);
const [points, setPoints] = useState<DataPoint[]>([]);

useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch("/api/worldmap");
if (!response.ok) return;
const data: DataPoint[] = await response.json();
dataPoints.current = data;
setPoints(data);
} catch {
// silently fail
}
};

fetchData();
const timer = setInterval(fetchData, 60000);
return () => clearInterval(timer);
}, []);

return (
<div className="relative w-full max-w-[1056px] mx-auto">
{/* Map container */}
<div className="relative mt-12 px-[5px] pb-[6%] pt-[2%] md:pb-[90px] md:pt-[25px]">
{points.map((data, idx) => (
<Marker key={idx} data={data} />
))}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/illustrations/world-map/map.svg"
width={1036}
height={609}
alt="World map"
className="w-full h-auto translate-x-[5px]"
/>

{/* Legend */}
<div className="absolute grid grid-cols-[repeat(4,auto)] gap-4 text-[10px] text-foreground-neutral-weak border border-stroke-neutral rounded-lg px-2.5 py-1.5 w-max -bottom-11 left-1/2 -translate-x-1/2 md:grid-cols-[auto_1fr] md:row-gap-0.5 md:col-gap-1.5 md:mb-[5%] md:bottom-0 md:left-0 md:translate-x-0 lg:mb-[84px]">
<span className="size-2.5 rounded-full bg-[#71E8DF99] border border-[#B7F4EE] shadow-[0_0_10px_0_#71E8DF99] self-center" />
<span className="self-center">Active Point of Presence</span>
<span className="size-2.5 rounded-full bg-[rgba(113,128,150,1)] border border-stroke-neutral self-center" />
<span className="self-center">Inactive Point of Presence</span>
</div>
</div>
</div>
);
}
81 changes: 81 additions & 0 deletions apps/site/src/app/global/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { Metadata } from "next";
import { Button } from "@prisma/eclipse";
import { WorldMap } from "./_components/world-map";

export const metadata: Metadata = {
title: "Global Traffic | Prisma",
description:
"Track real-time global traffic as developers build and scale with Prisma's commercial products.",
};

export default function GlobalPage() {
return (
<main className="flex-1 w-full z-1 -mt-24 pt-24 relative legal-hero-gradient">
<div className="max-w-[1056px] mx-auto px-2.5 pt-10 w-full md:mb-30">
{/* Hero */}
<div className="text-center">
<h1 className="text-5xl md:text-6xl font-bold font-sans-display text-foreground-neutral mt-10 mb-0 mx-auto">
Live Activity
</h1>
<p className="text-lg text-foreground-neutral-weak max-w-[780px] mx-auto mt-4 mb-6 text-balance">
Track real-time global traffic as developers build and scale with
our commercial products.
</p>
<div className="flex items-center justify-center gap-3 flex-col sm:flex-row [&>*]:w-full [&>*]:max-w-[300px] sm:[&>*]:w-auto">
<Button variant="ppg" size="xl" href="/accelerate">
Try Accelerate
</Button>
<Button variant="default-stronger" size="xl" href="/postgres">
Try Prisma Postgres
</Button>
</div>
</div>

{/* Map */}
<WorldMap />

{/* Footnote */}
<p className="text-center text-sm text-foreground-neutral-weak mt-20 md:mt-0 md:mb-20">
We pull our live usage data every 60 seconds to keep this map fresh.
Curious? Take a look at the Network tab.
</p>

{/* Share */}
<div className="text-center py-10">
<h3 className="text-lg font-bold text-foreground-neutral mb-4">
Share
</h3>
<div className="flex items-center justify-center gap-4">
<a
href="https://www.linkedin.com/sharing/share-offsite/?url=https://www.prisma.io/global"
target="_blank"
rel="noopener noreferrer"
className="text-foreground-neutral-weak hover:text-foreground-neutral transition-colors"
aria-label="Share on LinkedIn"
>
<i className="fa-brands fa-linkedin text-2xl" />
</a>
<a
href="https://twitter.com/intent/tweet?url=https://www.prisma.io/global&text=See%20Prisma%20Accelerate%27s%20real-time%20global%20traffic!"
target="_blank"
rel="noopener noreferrer"
className="text-foreground-neutral-weak hover:text-foreground-neutral transition-colors"
aria-label="Share on X"
>
<i className="fa-brands fa-x-twitter text-2xl" />
</a>
<a
href="https://bsky.app/intent/compose?text=See%20Prisma%20Accelerate%27s%20real-time%20global%20traffic!%20https://www.prisma.io/global"
target="_blank"
rel="noopener noreferrer"
className="text-foreground-neutral-weak hover:text-foreground-neutral transition-colors"
aria-label="Share on Bluesky"
>
<i className="fa-brands fa-bluesky text-2xl" />
</a>
</div>
</div>
</div>
</main>
);
}
Loading
Loading