Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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: translate(-50%, -50%) scale(0.9); opacity: 0.5; }
50% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}

@theme {
--animate-slide-down: slideDown 130s linear infinite;
--animate-slide-down-2: slideDown2 130s linear infinite;
Expand Down
114 changes: 114 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,114 @@
"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 markerRef = useRef<HTMLSpanElement>(null);
const [showTooltip, setShowTooltip] = useState(false);
const isActive = Boolean(data.ratio);

return (
<>
<span
ref={markerRef}
className={cn(
"absolute -translate-x-1/2 -translate-y-1/2 rounded-full z-1",
isActive
? "bg-[#71E8DF99] border border-[#B7F4EE] animate-[pulsate_2s_ease-in-out_infinite] shadow-[0_0_28px_0_#71E8DF99]"
: "size-[10px] bg-[rgba(113,128,150,1)] border border-[rgba(113,128,150,0.5)]",
)}
style={{
...(isActive && {
width: `${20 * (1 + (data.ratio || 0))}px`,
height: `${20 * (1 + (data.ratio || 0))}px`,
}),
...(data.cured_coord && {
left: `${data.cured_coord.lon}%`,
top: `${data.cured_coord.lat}%`,
}),
...(isActive && {
animationDuration: `${2 / (1 + (data.ratio || 0))}s`,
animationDelay: `${(data.ratio || 0) * 1000}ms`,
}),
}}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
/>
{showTooltip && isActive && data.pop && markerRef.current && (
<span
className="absolute z-10 px-2 py-1 text-xs font-semibold bg-background-neutral-strong text-foreground-neutral rounded -translate-x-1/2 pointer-events-none"
style={{
left: `${data.cured_coord.lon}%`,
top: `calc(${data.cured_coord.lat}% - 24px)`,
}}
>
{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-[1036px] mx-auto">
<div className="relative">
{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"
/>
</div>

{/* Legend */}
<div className="flex items-center gap-6 mt-6">
<div className="flex items-center gap-2">
<span className="size-3 rounded-full bg-[#71E8DF99] border border-[#B7F4EE] shadow-[0_0_10px_0_#71E8DF99]" />
<span className="text-sm text-foreground-neutral-weak">
Active Point of Presence
</span>
</div>
<div className="flex items-center gap-2">
<span className="size-3 rounded-full bg-[rgba(113,128,150,1)] border border-[rgba(113,128,150,0.5)]" />
<span className="text-sm text-foreground-neutral-weak">
Inactive Point of Presence
</span>
</div>
</div>
</div>
);
}
79 changes: 79 additions & 0 deletions apps/site/src/app/global/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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">
{/* Hero */}
<div className="text-center pt-16 pb-8 px-4">
<h1 className="text-5xl md:text-6xl font-bold font-sans-display text-foreground-neutral mb-4">
Live Activity
</h1>
<p className="text-lg text-foreground-neutral-weak max-w-[600px] mx-auto mb-8">
Track real-time global traffic as developers build and scale with our
commercial products.
</p>
<div className="flex items-center justify-center gap-4 flex-wrap">
<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 */}
<div className="px-4 py-12">
<WorldMap />
<p className="text-center text-sm text-foreground-neutral-weaker mt-6 max-w-[600px] mx-auto">
We pull our live usage data every 60 seconds to keep this map fresh.
Curious? Take a look at the Network tab.
</p>
</div>

{/* Share */}
<div className="text-center pb-20 px-4">
<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>
</main>
);
}
Loading
Loading