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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<div >
<a href="https://openknowledge.ai">website</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/inkeep/open-knowledge/releases/latest/download/OpenKnowledge-arm64.dmg">macOS app</a>
<a href="https://openknowledge.ai/download/stable">macOS app</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://openknowledge.ai/docs/get-started/quickstart#ok-install-web-app-linux-windows-intel-mac">web view + cli</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
Expand Down
5 changes: 4 additions & 1 deletion docs/src/app/(home)/marketing-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ export function MarketingButton({
const ArrowIcon = iconDirection === 'down' ? ArrowDown : ArrowRight;
const ChevronsIcon = iconDirection === 'down' ? ChevronsDown : ChevronsRight;
const isFileLink = typeof href === 'string' && href.split('?')[0]?.toLowerCase().endsWith('.pdf');
const isRedirectRoute =
typeof href === 'string' && (href.startsWith('/download/') || href.startsWith('/updates/'));
const useRawAnchor = isFileLink || isRedirectRoute;
const computedDownload = typeof download !== 'undefined' ? download : isFileLink ? '' : undefined;

const buttonContent = (
Expand Down Expand Up @@ -251,7 +254,7 @@ export function MarketingButton({
const defaultRel = target === '_blank' ? 'noopener noreferrer' : undefined;
const finalRel = rel || defaultRel;

if (isFileLink) {
if (useRawAnchor) {
return (
<a
href={href}
Expand Down
4 changes: 2 additions & 2 deletions docs/src/app/(home)/sections/call-to-action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import Image from 'next/image';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { DOWNLOAD_URL } from '@/lib/site';
import { DOWNLOAD_ROUTE } from '@/lib/site';
import { useIsInView } from '@/lib/use-is-in-view';
import { cn } from '@/lib/utils';
import { DotTexture } from '../dot-texture';
Expand Down Expand Up @@ -104,7 +104,7 @@ export function CallToAction() {

<div className="mt-10 flex flex-wrap items-center justify-center gap-5">
<MarketingButton
href={DOWNLOAD_URL}
href={DOWNLOAD_ROUTE}
target="_blank"
size="lg"
showIcon
Expand Down
4 changes: 2 additions & 2 deletions docs/src/app/(home)/sections/hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useState } from 'react';
import { ClaudeIcon } from '@/components/icons/claude';
import { CodexBrandIcon } from '@/components/icons/codex';
import { CursorIcon } from '@/components/icons/cursor';
import { DOWNLOAD_URL, SITE_HEADLINE } from '@/lib/site';
import { DOWNLOAD_ROUTE, SITE_HEADLINE } from '@/lib/site';
import { cn } from '@/lib/utils';
import { DotTexture } from '../dot-texture';
import { MarketingButton } from '../marketing-button';
Expand Down Expand Up @@ -46,7 +46,7 @@ export function Hero() {

<div className="mt-6 flex items-center justify-center gap-4">
<MarketingButton
href={DOWNLOAD_URL}
href={DOWNLOAD_ROUTE}
target="_blank"
size="md"
showIcon
Expand Down
6 changes: 3 additions & 3 deletions docs/src/app/(home)/site-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DiscordIcon } from '@/components/icons/discord';
import { GitHubIcon } from '@/components/icons/github';
import { XIcon } from '@/components/icons/x';
import { OkWordmark } from '@/components/ok-wordmark';
import { DOWNLOAD_URL } from '@/lib/site';
import { DOWNLOAD_ROUTE } from '@/lib/site';
import { MarketingButton } from './marketing-button';

type NavLink = {
Expand Down Expand Up @@ -127,7 +127,7 @@ export function SiteNav() {
</Link>
);
})}
<MarketingButton href={DOWNLOAD_URL} size="sm">
<MarketingButton href={DOWNLOAD_ROUTE} size="sm">
Download
</MarketingButton>
</nav>
Expand Down Expand Up @@ -186,7 +186,7 @@ export function SiteNav() {
</Link>
);
})}
<MarketingButton href={DOWNLOAD_URL} size="md" className="text-base" showIcon>
<MarketingButton href={DOWNLOAD_ROUTE} size="md" className="text-base" showIcon>
Download
</MarketingButton>
</nav>
Expand Down
4 changes: 2 additions & 2 deletions docs/src/app/continue/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
PENDING_SHARE_COOKIE,
PORT_PARAM,
} from '@/lib/deferred-share';
import { SPLASH_DOWNLOAD_URL } from '@/lib/share-splash';
import { DOWNLOAD_ROUTE } from '@/lib/site';

export const dynamic = 'force-dynamic';

Expand Down Expand Up @@ -68,7 +68,7 @@ export default async function ContinuePage({ searchParams }: ContinuePageProps)
<br />
Don&rsquo;t have the app yet?{' '}
<a
href={SPLASH_DOWNLOAD_URL}
href={DOWNLOAD_ROUTE}
className="font-medium text-slide-text underline underline-offset-4 transition-colors hover:text-primary"
>
Download it for macOS
Expand Down
70 changes: 70 additions & 0 deletions docs/src/app/d/[encoded]/download/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, mock, test } from 'bun:test';

const SPLASH_URL =
'https://github.com/inkeep/open-knowledge/releases/latest/download/OpenKnowledge-arm64.dmg';

type CaptureOpts = {
event: string;
distinctId: string;
properties?: Record<string, string | undefined>;
};
let _lastCapture: CaptureOpts | null = null;
mock.module('../../../../lib/track.ts', () => ({
captureServerEvent: (opts: CaptureOpts) => {
_lastCapture = opts;
},
resolveDistinctId: () => 'splash-1',
}));

let _viewKind: 'ok' | 'invalid' | 'unsupported-version' = 'ok';
mock.module('../../../../lib/share-splash.ts', () => ({
buildSplashViewModel: () => ({ kind: _viewKind }),
SPLASH_DOWNLOAD_URL: SPLASH_URL,
}));

mock.module('../../../../lib/deferred-share.ts', () => ({
buildPendingShareCookie: (encoded: string) => ({ name: 'ok-pending-share', value: encoded }),
}));

const { GET } = await import('./route.ts');

function call(encoded: string): Promise<Response> {
return GET(new Request(`https://openknowledge.ai/d/${encoded}/download`), {
params: Promise.resolve({ encoded }),
});
}

describe('GET /d/[encoded]/download', () => {
test('valid share: 302 to the DMG, sets the pairing cookie, counts share-splash', async () => {
_viewKind = 'ok';
_lastCapture = null;
const res = await call('valid-share');
expect(res.status).toBe(302);
expect(res.headers.get('location')).toBe(SPLASH_URL);
expect(res.headers.get('set-cookie')).toContain('ok-pending-share=valid-share');
expect(_lastCapture?.event).toBe('dmg_downloaded');
expect(_lastCapture?.properties?.channel).toBe('stable');
expect(_lastCapture?.properties?.source).toBe('share-splash');
expect(_lastCapture?.distinctId).toBe('splash-1');
});

test('invalid share: 302 with NO cookie but still counts the download', async () => {
_viewKind = 'invalid';
_lastCapture = null;
const res = await call('bad-share');
expect(res.status).toBe(302);
expect(res.headers.get('location')).toBe(SPLASH_URL);
expect(res.headers.get('set-cookie')).toBeNull();
expect(_lastCapture?.event).toBe('dmg_downloaded');
expect(_lastCapture?.properties?.source).toBe('share-splash');
});

test('unsupported-version share: no cookie, still counts', async () => {
_viewKind = 'unsupported-version';
_lastCapture = null;
const res = await call('old-share');
expect(res.status).toBe(302);
expect(res.headers.get('set-cookie')).toBeNull();
expect(_lastCapture?.event).toBe('dmg_downloaded');
});
});
9 changes: 8 additions & 1 deletion docs/src/app/d/[encoded]/download/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { NextResponse } from 'next/server';
import { buildPendingShareCookie } from '@/lib/deferred-share';
import { buildSplashViewModel, SPLASH_DOWNLOAD_URL } from '@/lib/share-splash';
import { captureServerEvent, resolveDistinctId } from '@/lib/track';

export async function GET(
_request: Request,
request: Request,
{ params }: { params: Promise<{ encoded: string }> },
): Promise<NextResponse> {
const { encoded } = await params;
Expand All @@ -14,5 +15,11 @@ export async function GET(
response.cookies.set(buildPendingShareCookie(encoded));
}

captureServerEvent({
event: 'dmg_downloaded',
distinctId: resolveDistinctId(request),
properties: { channel: 'stable', source: 'share-splash' },
});

return response;
}
10 changes: 3 additions & 7 deletions docs/src/app/d/[encoded]/splash-share-view.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { DotIcon, GitBranchIcon } from 'lucide-react';
import Link from 'next/link';
import { OkWordmark } from '@/components/ok-wordmark';
import {
buildCloneCommand,
SPLASH_DOWNLOAD_URL,
SPLASH_INSTALL_COMMAND,
type SplashView,
} from '@/lib/share-splash';
import { buildCloneCommand, SPLASH_INSTALL_COMMAND, type SplashView } from '@/lib/share-splash';
import { DOWNLOAD_ROUTE } from '@/lib/site';
import { DotTexture } from '../../(home)/dot-texture';
import { SiteFooter } from '../../(home)/footer';
import { SplashButtonLabel, splashPrimaryButton } from './splash-buttons';
Expand Down Expand Up @@ -133,7 +129,7 @@ export function SplashFallback({ heading }: { heading: string }) {
</h1>

<div className="mt-10 flex flex-wrap items-center gap-4">
<a href={SPLASH_DOWNLOAD_URL} className={splashPrimaryButton}>
<a href={DOWNLOAD_ROUTE} className={splashPrimaryButton}>
<SplashButtonLabel direction="down">DOWNLOAD FOR MAC</SplashButtonLabel>
</a>
<SplashCliButton installCommand={SPLASH_INSTALL_COMMAND} />
Expand Down
38 changes: 32 additions & 6 deletions docs/src/app/download/beta/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,56 @@ mock.module('../../../lib/download-links.ts', () => ({
}),
}));

type CaptureOpts = {
event: string;
distinctId: string;
properties?: Record<string, string | undefined>;
};
let _lastCapture: CaptureOpts | null = null;
mock.module('../../../lib/track.ts', () => ({
captureServerEvent: (opts: CaptureOpts) => {
_lastCapture = opts;
},
resolveDistinctId: () => 'visitor-9',
referrerHostname: () => undefined,
}));

const { GET } = await import('./route.ts');

function call(): Promise<Response> {
return GET(new Request('https://openknowledge.ai/download/beta'));
}

describe('GET /download/beta', () => {
test('302 to the fresh beta URL with CDN-cacheable headers', async () => {
test('302 to the fresh beta URL and fires dmg_downloaded (beta)', async () => {
_redirect = { kind: 'fresh', url: TEST_DMG_URL };
const res = await GET();
_lastCapture = null;
const res = await call();
expect(res.status).toBe(302);
expect(res.headers.get('location')).toBe(TEST_DMG_URL);
expect(res.headers.get('cache-control')).toBe(SUCCESS_CACHE_CONTROL);
expect(_lastCapture?.event).toBe('dmg_downloaded');
expect(_lastCapture?.distinctId).toBe('visitor-9');
expect(_lastCapture?.properties?.channel).toBe('beta');
});

test('302 to the stale LKG URL with CDN-cacheable headers', async () => {
test('302 to the stale LKG URL and still counts', async () => {
_redirect = { kind: 'stale-lkg', url: TEST_DMG_URL, refreshError: 'network down' };
const res = await GET();
_lastCapture = null;
const res = await call();
expect(res.status).toBe(302);
expect(res.headers.get('location')).toBe(TEST_DMG_URL);
expect(res.headers.get('cache-control')).toBe(SUCCESS_CACHE_CONTROL);
expect(_lastCapture?.properties?.channel).toBe('beta');
});

test('302 to the releases page uncached on fallback', async () => {
test('302 to the releases page on fallback is NOT counted as a download', async () => {
_redirect = { kind: 'fallback', url: RELEASES_PAGE_URL, cause: 'API error' };
const res = await GET();
_lastCapture = null;
const res = await call();
expect(res.status).toBe(302);
expect(res.headers.get('location')).toBe(RELEASES_PAGE_URL);
expect(res.headers.get('cache-control')).toBe(FALLBACK_CACHE_CONTROL);
expect(_lastCapture).toBeNull();
});
});
10 changes: 9 additions & 1 deletion docs/src/app/download/beta/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createBetaResolver, toRedirectResponse } from '@/lib/download-links';
import { captureServerEvent, referrerHostname, resolveDistinctId } from '@/lib/track';

export const dynamic = 'force-dynamic';

const resolveBetaRedirect = createBetaResolver();

export async function GET(): Promise<Response> {
export async function GET(request: Request): Promise<Response> {
const redirect = await resolveBetaRedirect();
if (redirect.kind === 'stale-lkg') {
console.warn(
Expand All @@ -14,5 +15,12 @@ export async function GET(): Promise<Response> {
if (redirect.kind === 'fallback') {
console.error(`[download/beta] falling back to releases page: ${redirect.cause}`);
}
if (redirect.kind !== 'fallback') {
captureServerEvent({
event: 'dmg_downloaded',
distinctId: resolveDistinctId(request),
properties: { channel: 'beta', referrer: referrerHostname(request) },
});
}
return toRedirectResponse(redirect);
}
33 changes: 27 additions & 6 deletions docs/src/app/download/stable/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
import { describe, expect, test } from 'bun:test';
import { STABLE_CACHE_CONTROL, STABLE_DMG_URL } from '../../../lib/download-links.ts';
import { GET } from './route.ts';
import { describe, expect, mock, test } from 'bun:test';
import { STABLE_DMG_URL } from '../../../lib/download-links.ts';

type CaptureOpts = {
event: string;
distinctId: string;
properties?: Record<string, string | undefined>;
};

let _lastCapture: CaptureOpts | null = null;
mock.module('../../../lib/track.ts', () => ({
captureServerEvent: (opts: CaptureOpts) => {
_lastCapture = opts;
},
resolveDistinctId: () => 'visitor-1',
referrerHostname: () => 'news.ycombinator.com',
}));

const { GET } = await import('./route.ts');

describe('GET /download/stable', () => {
test('302 to the stable DMG URL with CDN-cacheable headers', () => {
const res = GET();
test('302 to the stable DMG URL, uncached, and fires dmg_downloaded', () => {
_lastCapture = null;
const res = GET(new Request('https://openknowledge.ai/download/stable'));
expect(res.status).toBe(302);
expect(res.headers.get('location')).toBe(STABLE_DMG_URL);
expect(res.headers.get('cache-control')).toBe(STABLE_CACHE_CONTROL);
expect(res.headers.get('cache-control')).toBe('no-store');
expect(_lastCapture?.event).toBe('dmg_downloaded');
expect(_lastCapture?.distinctId).toBe('visitor-1');
expect(_lastCapture?.properties?.channel).toBe('stable');
expect(_lastCapture?.properties?.referrer).toBe('news.ycombinator.com');
});
});
12 changes: 9 additions & 3 deletions docs/src/app/download/stable/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { STABLE_CACHE_CONTROL, STABLE_DMG_URL } from '@/lib/download-links';
import { STABLE_DMG_URL } from '@/lib/download-links';
import { captureServerEvent, referrerHostname, resolveDistinctId } from '@/lib/track';

export const dynamic = 'force-dynamic';

export function GET(): Response {
export function GET(request: Request): Response {
captureServerEvent({
event: 'dmg_downloaded',
distinctId: resolveDistinctId(request),
properties: { channel: 'stable', referrer: referrerHostname(request) },
});
return new Response(null, {
status: 302,
headers: {
location: STABLE_DMG_URL,
'cache-control': STABLE_CACHE_CONTROL,
'cache-control': 'no-store',
},
});
}
Loading