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
30 changes: 28 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
NETWORK_DASHBOARD_PLATFORM_URL: "${{ secrets.NETWORK_DASHBOARD_PLATFORM_URL }}"
STELLAR_NETWORK: "${{ secrets.STELLAR_NETWORK }}"
RPC_URL: "${{ secrets.RPC_URL }}"
POSTHOG_KEY: "${{ secrets.POSTHOG_PROJECT_TOKEN }}"
steps:
- uses: actions/checkout@v4

Expand All @@ -22,7 +23,7 @@ jobs:
- name: Validate testnet config inputs
run: |
set -euo pipefail
for var in COUNCIL_PLATFORM_URL NETWORK_DASHBOARD_PLATFORM_URL STELLAR_NETWORK RPC_URL; do
for var in COUNCIL_PLATFORM_URL NETWORK_DASHBOARD_PLATFORM_URL STELLAR_NETWORK RPC_URL POSTHOG_KEY; do
if [ -z "${!var}" ]; then
echo "::error::Required env var $var is empty"
exit 1
Expand All @@ -49,6 +50,8 @@ jobs:
rpcUrl: Deno.env.get('RPC_URL'),
councilPlatformUrl: Deno.env.get('COUNCIL_PLATFORM_URL'),
networkDashboardPlatformUrl: Deno.env.get('NETWORK_DASHBOARD_PLATFORM_URL'),
posthogKey: Deno.env.get('POSTHOG_KEY'),
posthogHost: 'https://us.i.posthog.com',
};
Deno.writeTextFileSync('public/config.js',
'window.__DASHBOARD_CONFIG__ = ' + JSON.stringify(config, null, 2) + ';\n');
Expand All @@ -58,6 +61,15 @@ jobs:
- name: Build production bundle
run: deno task build -- --production

- name: Upload source maps to PostHog
env:
POSTHOG_CLI_API_KEY: "${{ secrets.POSTHOG_CLI_API_KEY }}"
POSTHOG_CLI_PROJECT_ID: "345524"
POSTHOG_CLI_HOST: "https://us.posthog.com"
run: |
npx --yes @posthog/cli sourcemap inject --directory public/
npx --yes @posthog/cli sourcemap upload --directory public/

- name: Deploy to Tigris (testnet)
env:
AWS_ACCESS_KEY_ID: "${{ secrets.TIGRIS_ACCESS_KEY_ID }}"
Expand All @@ -68,6 +80,7 @@ jobs:
--endpoint-url https://fly.storage.tigris.dev \
--acl public-read \
--delete \
--exclude "*.map" \
--no-progress
# config.js must NOT be edge-cached — it carries the WS URL and other
# deploy-time substitutions. Other assets keep the bucket default.
Expand All @@ -86,6 +99,7 @@ jobs:
NETWORK_DASHBOARD_PLATFORM_URL: "${{ secrets.MAINNET_NETWORK_DASHBOARD_PLATFORM_URL }}"
STELLAR_NETWORK: "${{ secrets.MAINNET_STELLAR_NETWORK }}"
RPC_URL: "${{ secrets.MAINNET_RPC_URL }}"
POSTHOG_KEY: "${{ secrets.POSTHOG_PROJECT_TOKEN }}"
steps:
- uses: actions/checkout@v4

Expand All @@ -96,7 +110,7 @@ jobs:
- name: Validate mainnet config inputs
run: |
set -euo pipefail
for var in COUNCIL_PLATFORM_URL NETWORK_DASHBOARD_PLATFORM_URL STELLAR_NETWORK RPC_URL; do
for var in COUNCIL_PLATFORM_URL NETWORK_DASHBOARD_PLATFORM_URL STELLAR_NETWORK RPC_URL POSTHOG_KEY; do
if [ -z "${!var}" ]; then
echo "::error::Required env var $var is empty"
exit 1
Expand All @@ -123,6 +137,8 @@ jobs:
rpcUrl: Deno.env.get('RPC_URL'),
councilPlatformUrl: Deno.env.get('COUNCIL_PLATFORM_URL'),
networkDashboardPlatformUrl: Deno.env.get('NETWORK_DASHBOARD_PLATFORM_URL'),
posthogKey: Deno.env.get('POSTHOG_KEY'),
posthogHost: 'https://us.i.posthog.com',
};
Deno.writeTextFileSync('public/config.js',
'window.__DASHBOARD_CONFIG__ = ' + JSON.stringify(config, null, 2) + ';\n');
Expand All @@ -132,6 +148,15 @@ jobs:
- name: Build production bundle
run: deno task build -- --production

- name: Upload source maps to PostHog
env:
POSTHOG_CLI_API_KEY: "${{ secrets.POSTHOG_CLI_API_KEY }}"
POSTHOG_CLI_PROJECT_ID: "345524"
POSTHOG_CLI_HOST: "https://us.posthog.com"
run: |
npx --yes @posthog/cli sourcemap inject --directory public/
npx --yes @posthog/cli sourcemap upload --directory public/

- name: Deploy to Tigris (mainnet)
env:
AWS_ACCESS_KEY_ID: "${{ secrets.MAINNET_TIGRIS_ACCESS_KEY_ID }}"
Expand All @@ -142,6 +167,7 @@ jobs:
--endpoint-url https://fly.storage.tigris.dev \
--acl public-read \
--delete \
--exclude "*.map" \
--no-progress
# config.js must NOT be edge-cached — it carries the WS URL and other
# deploy-time substitutions. Other assets keep the bucket default.
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.2.14",
"version": "0.2.15",
"license": "MIT",
"tasks": {
"dev": "deno run --allow-all --watch src/server.ts",
Expand Down
3 changes: 3 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { renderNav } from "@moonlight/ui/nav";
import { pageLayout } from "@moonlight/ui/layout";
import { NETWORK_DASHBOARD_PLATFORM_URL } from "./lib/config.ts";
import { initAnalytics } from "./lib/analytics.ts";
import { connectNetworkPlatform } from "./lib/ws-client.ts";
import { CounterStrip } from "./views/counter-strip.ts";
import { ActivityFeed } from "./views/activity-feed.ts";
Expand Down Expand Up @@ -95,6 +96,8 @@ function renderShell(): {
};
}

initAnalytics();

function bootstrap() {
const {
counters,
Expand Down
2 changes: 1 addition & 1 deletion src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ await esbuild.build({
platform: "browser",
target: "es2022",
minify: isProduction,
sourcemap: !isProduction,
sourcemap: true,
define: {
"__APP_VERSION__": JSON.stringify(version),
},
Expand Down
49 changes: 49 additions & 0 deletions src/lib/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { IS_PRODUCTION, POSTHOG_HOST, POSTHOG_KEY } from "./config.ts";

/**
* PostHog error-tracking wrapper.
* NOOP in development; in production, autocaptures unhandled exceptions and
* exposes a manual `captureException` for caught error paths.
*/

interface Analytics {
captureException(error: unknown, properties?: Record<string, unknown>): void;
}

const noop: Analytics = {
captureException() {},
};

let analytics: Analytics = noop;

export function initAnalytics(): void {
if (!IS_PRODUCTION || !POSTHOG_KEY) {
return;
}

const script = document.createElement("script");
script.src = "https://us-assets.i.posthog.com/static/array.js";
script.onload = () => {
// deno-lint-ignore no-explicit-any
const posthog = (window as any).posthog;
if (posthog) {
posthog.init(POSTHOG_KEY, {
api_host: POSTHOG_HOST,
capture_exceptions: true,
person_profiles: "identified_only",
});
analytics = {
captureException: (error, properties) =>
posthog.captureException(error, properties),
};
}
};
document.head.appendChild(script);
}

export function captureException(
error: unknown,
properties?: Record<string, unknown>,
): void {
analytics.captureException(error, properties);
}
5 changes: 5 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ interface DashboardConfig {
environment?: string;
stellarNetwork?: "testnet" | "mainnet" | "standalone";
networkDashboardPlatformUrl?: string;
posthogKey?: string;
posthogHost?: string;
}

declare global {
Expand Down Expand Up @@ -43,3 +45,6 @@ export const STELLAR_NETWORK = c.stellarNetwork ?? "testnet";
*/
export const NETWORK_DASHBOARD_PLATFORM_URL: string =
(c.networkDashboardPlatformUrl ?? "").trim();

export const POSTHOG_KEY = c.posthogKey ?? "";
export const POSTHOG_HOST = c.posthogHost ?? "https://us.i.posthog.com";
4 changes: 2 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ function getCSP(): string {
// blocked. Production allows-list the dashboard-api WS hosts the SPA
// connects to; ws/wss schemes need explicit hosts even with 'self'.
const connectSrc = isProd
? "connect-src 'self' wss://dashboard-api.moonlightprotocol.io wss://dashboard-api-testnet.moonlightprotocol.io"
? "connect-src 'self' wss://dashboard-api.moonlightprotocol.io wss://dashboard-api-testnet.moonlightprotocol.io https://us.i.posthog.com"
: "connect-src *";
// §4 asset-breakdown bars and a couple of inline transitions render via
// `style="..."`. The dashboard is public + anonymous, no auth boundary
// to defend, so 'unsafe-inline' on style-src is an acceptable trade.
const styleSrc = "style-src 'self' 'unsafe-inline'";
return [
"default-src 'self'",
"script-src 'self'",
"script-src 'self' https://us-assets.i.posthog.com",
styleSrc,
connectSrc,
].join("; ");
Expand Down
Loading