Skip to content
Merged
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
6 changes: 4 additions & 2 deletions apps/landing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ overrides the ingestion host (default `https://us.i.posthog.com`).

Download CTAs point at the first-party `/download/macos` Worker redirect. The
Worker sends the download click event server-side with the runtime
`LANDING_POSTHOG_KEY` secret, then returns a non-cacheable 302 to the GitHub
release URL. This keeps download click counting working when client-side
`LANDING_POSTHOG_KEY` secret, resolves the current `.dmg` asset from the
`desktop-version.json` release feed, then returns a non-cacheable 302 to that
installer. If the feed cannot be resolved, the Worker falls back to the GitHub
release page. This keeps download click counting working when client-side
analytics is blocked, while the download still succeeds if server-side tracking
is unavailable.

Expand Down
1 change: 1 addition & 0 deletions apps/landing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"clean": "rimraf dist",
"lint": "eslint src --ext .ts,.tsx",
"typecheck": "tsc --noEmit",
"test": "vitest run --config vitest.config.ts",
"deploy": "wrangler deploy"
},
"dependencies": {
Expand Down
2 changes: 2 additions & 0 deletions apps/landing/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ function GitHubLink({ placement, className, children }: CtaLinkProps) {
<a
className={className}
href={GITHUB_URL}
target="_blank"
rel="noreferrer"
onClick={() =>
trackLandingEvent({
name: "landing_github_clicked",
Expand Down
5 changes: 4 additions & 1 deletion apps/landing/src/site.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export const GITHUB_URL = "https://github.com/ymichael/bb";
export const DOWNLOAD_MACOS_URL =
export const DOWNLOAD_MACOS_FALLBACK_URL =
"https://github.com/ymichael/bb/releases/tag/desktop-latest";
export const DOWNLOAD_MACOS_RELEASE_ASSET_BASE_URL =
"https://github.com/ymichael/bb/releases/download/desktop-latest";
export const DOWNLOAD_MACOS_VERSION_FEED_URL = `${DOWNLOAD_MACOS_RELEASE_ASSET_BASE_URL}/desktop-version.json`;
export const DOWNLOAD_MACOS_REDIRECT_PATH = "/download/macos";
/** First-party endpoint that adds an email to the bb marketing audience.
* Handled by the Worker (see worker.ts), not a prerendered asset. */
Expand Down
75 changes: 75 additions & 0 deletions apps/landing/src/worker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { afterEach, describe, expect, it, vi } from "vitest";

import {
DOWNLOAD_MACOS_FALLBACK_URL,
DOWNLOAD_MACOS_RELEASE_ASSET_BASE_URL,
DOWNLOAD_MACOS_VERSION_FEED_URL,
} from "./site";
import worker from "./worker";

function makeEnv() {
return {
ASSETS: {
fetch: vi.fn(async () => new Response("asset")),
},
};
}

function makeContext() {
return {
waitUntil: vi.fn(),
};
}

describe("landing worker download redirect", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});

it("redirects macOS downloads to the current dmg asset", async () => {
const fetchMock = vi.fn(async () => {
return new Response(
JSON.stringify({
files: [
{ url: "bb-0.0.26-arm64.zip" },
{ url: "bb-0.0.26-arm64.dmg" },
],
}),
);
});
vi.stubGlobal("fetch", fetchMock);

const response = await worker.fetch(
new Request("https://getbb.app/download/macos?placement=hero"),
makeEnv(),
makeContext(),
);

expect(fetchMock).toHaveBeenCalledWith(DOWNLOAD_MACOS_VERSION_FEED_URL, {
headers: { accept: "application/json" },
});
expect(response.status).toBe(302);
expect(response.headers.get("Location")).toBe(
`${DOWNLOAD_MACOS_RELEASE_ASSET_BASE_URL}/bb-0.0.26-arm64.dmg`,
);
});

it("falls back to the release page when the feed has no dmg", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => {
return new Response(JSON.stringify({ files: [{ url: "notes.txt" }] }));
}),
);

const response = await worker.fetch(
new Request("https://getbb.app/download/macos"),
makeEnv(),
makeContext(),
);

expect(response.status).toBe(302);
expect(response.headers.get("Location")).toBe(DOWNLOAD_MACOS_FALLBACK_URL);
});
});
63 changes: 60 additions & 3 deletions apps/landing/src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
DOWNLOAD_MACOS_FALLBACK_URL,
DOWNLOAD_MACOS_RELEASE_ASSET_BASE_URL,
DOWNLOAD_MACOS_REDIRECT_PATH,
DOWNLOAD_MACOS_URL,
DOWNLOAD_MACOS_VERSION_FEED_URL,
SUBSCRIBE_PATH,
} from "./site";
import type { CtaPlacement } from "./site";
Expand All @@ -12,6 +14,7 @@ const TRACKING_SOURCE = "landing_worker_redirect";
const MAX_URL_PROPERTY_LENGTH = 2048;
const RESEND_CONTACTS_URL = "https://api.resend.com/audiences";
const MAX_EMAIL_LENGTH = 254;
const MACOS_INSTALLER_EXTENSION = ".dmg";
// Permissive single-line email shape; Resend does the authoritative validation.
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

Expand Down Expand Up @@ -180,16 +183,70 @@ function isDownloadMacosRequest(requestUrl: URL): boolean {
);
}

function redirectToMacosDownload(): Response {
async function redirectToMacosDownload(): Promise<Response> {
const location = await resolveMacosDownloadUrl();
return redirectResponse(location);
}

function redirectResponse(location: string): Response {
return new Response(null, {
headers: {
"Cache-Control": "no-store",
Location: DOWNLOAD_MACOS_URL,
Location: location,
},
status: 302,
});
}

async function resolveMacosDownloadUrl(): Promise<string> {
try {
const response = await fetch(DOWNLOAD_MACOS_VERSION_FEED_URL, {
headers: { accept: "application/json" },
});
if (!response.ok) {
return DOWNLOAD_MACOS_FALLBACK_URL;
}

const assetName = findMacosInstallerAssetName(await response.json());
if (!assetName) {
return DOWNLOAD_MACOS_FALLBACK_URL;
}

return `${DOWNLOAD_MACOS_RELEASE_ASSET_BASE_URL}/${encodeURIComponent(assetName)}`;
} catch {
return DOWNLOAD_MACOS_FALLBACK_URL;
}
}

function findMacosInstallerAssetName(feed: unknown): string | null {
if (!isRecord(feed) || !Array.isArray(feed.files)) {
return null;
}

for (const file of feed.files) {
if (!isRecord(file) || typeof file.url !== "string") {
continue;
}
if (isMacosInstallerAssetName(file.url)) {
return file.url;
}
}
return null;
}

function isMacosInstallerAssetName(value: string): boolean {
return (
value.length > MACOS_INSTALLER_EXTENSION.length &&
value.endsWith(MACOS_INSTALLER_EXTENSION) &&
!value.includes("/") &&
!value.includes("\\")
);
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

async function trackDownloadClick(
args: TrackDownloadClickArgs,
): Promise<void> {
Expand Down
10 changes: 10 additions & 0 deletions apps/landing/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineWorkspaceTestConfig } from "../../vitest.shared.js";

export default defineWorkspaceTestConfig({
test: {
silent: "passed-only",
name: "@bb/landing",
include: ["src/**/*.test.ts"],
exclude: ["dist/**", "node_modules/**"],
},
});
Loading