Connor Fogarty
commited on
feat: add basic analytics (#29)
Browse files- packages/bolt/app/components/chat/Chat.client.tsx +13 -0
- packages/bolt/app/lib/.server/sessions.ts +10 -1
- packages/bolt/app/lib/analytics.ts +103 -0
- packages/bolt/app/lib/persistence/useChatHistory.ts +11 -0
- packages/bolt/app/root.tsx +17 -1
- packages/bolt/app/routes/api.analytics.ts +20 -0
- packages/bolt/app/routes/login.tsx +8 -2
- packages/bolt/package.json +1 -0
- pnpm-lock.yaml +95 -0
packages/bolt/app/components/chat/Chat.client.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { fileModificationsToHTML } from '~/utils/diff';
|
|
| 11 |
import { cubicEasingFn } from '~/utils/easings';
|
| 12 |
import { createScopedLogger, renderLogger } from '~/utils/logger';
|
| 13 |
import { BaseChat } from './BaseChat';
|
|
|
|
| 14 |
|
| 15 |
const toastAnimation = cssTransition({
|
| 16 |
enter: 'animated fadeInRight',
|
|
@@ -191,6 +192,18 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
|
|
| 191 |
resetEnhancer();
|
| 192 |
|
| 193 |
textareaRef.current?.blur();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
};
|
| 195 |
|
| 196 |
const [messageRef, scrollRef] = useSnapScroll();
|
|
|
|
| 11 |
import { cubicEasingFn } from '~/utils/easings';
|
| 12 |
import { createScopedLogger, renderLogger } from '~/utils/logger';
|
| 13 |
import { BaseChat } from './BaseChat';
|
| 14 |
+
import { sendAnalyticsEvent, AnalyticsTrackEvent, AnalyticsAction } from '~/lib/analytics';
|
| 15 |
|
| 16 |
const toastAnimation = cssTransition({
|
| 17 |
enter: 'animated fadeInRight',
|
|
|
|
| 192 |
resetEnhancer();
|
| 193 |
|
| 194 |
textareaRef.current?.blur();
|
| 195 |
+
|
| 196 |
+
const event = messages.length === 0 ? AnalyticsTrackEvent.ChatCreated : AnalyticsTrackEvent.MessageSent;
|
| 197 |
+
|
| 198 |
+
sendAnalyticsEvent({
|
| 199 |
+
action: AnalyticsAction.Track,
|
| 200 |
+
payload: {
|
| 201 |
+
event,
|
| 202 |
+
properties: {
|
| 203 |
+
message: _input,
|
| 204 |
+
},
|
| 205 |
+
},
|
| 206 |
+
});
|
| 207 |
};
|
| 208 |
|
| 209 |
const [messageRef, scrollRef] = useSnapScroll();
|
packages/bolt/app/lib/.server/sessions.ts
CHANGED
|
@@ -3,12 +3,15 @@ import { decodeJwt } from 'jose';
|
|
| 3 |
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
|
| 4 |
import { request as doRequest } from '~/lib/fetch';
|
| 5 |
import { logger } from '~/utils/logger';
|
|
|
|
| 6 |
|
| 7 |
const DEV_SESSION_SECRET = import.meta.env.DEV ? 'LZQMrERo3Ewn/AbpSYJ9aw==' : undefined;
|
| 8 |
|
| 9 |
interface SessionData {
|
| 10 |
refresh: string;
|
| 11 |
expiresAt: number;
|
|
|
|
|
|
|
| 12 |
}
|
| 13 |
|
| 14 |
export async function isAuthenticated(request: Request, env: Env) {
|
|
@@ -50,6 +53,7 @@ export async function createUserSession(
|
|
| 50 |
request: Request,
|
| 51 |
env: Env,
|
| 52 |
tokens: { refresh: string; expires_in: number; created_at: number },
|
|
|
|
| 53 |
): Promise<ResponseInit> {
|
| 54 |
const { session, sessionStorage } = await getSession(request, env);
|
| 55 |
|
|
@@ -58,6 +62,11 @@ export async function createUserSession(
|
|
| 58 |
session.set('refresh', tokens.refresh);
|
| 59 |
session.set('expiresAt', expiresAt);
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
return {
|
| 62 |
headers: {
|
| 63 |
'Set-Cookie': await sessionStorage.commitSession(session, {
|
|
@@ -97,7 +106,7 @@ export function validateAccessToken(access: string) {
|
|
| 97 |
return jwtPayload.bolt === true;
|
| 98 |
}
|
| 99 |
|
| 100 |
-
async function getSession(request: Request, env: Env) {
|
| 101 |
const sessionStorage = getSessionStorage(env);
|
| 102 |
const cookie = request.headers.get('Cookie');
|
| 103 |
|
|
|
|
| 3 |
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
|
| 4 |
import { request as doRequest } from '~/lib/fetch';
|
| 5 |
import { logger } from '~/utils/logger';
|
| 6 |
+
import type { Identity } from '~/lib/analytics';
|
| 7 |
|
| 8 |
const DEV_SESSION_SECRET = import.meta.env.DEV ? 'LZQMrERo3Ewn/AbpSYJ9aw==' : undefined;
|
| 9 |
|
| 10 |
interface SessionData {
|
| 11 |
refresh: string;
|
| 12 |
expiresAt: number;
|
| 13 |
+
userId: string | null;
|
| 14 |
+
segmentWriteKey: string | null;
|
| 15 |
}
|
| 16 |
|
| 17 |
export async function isAuthenticated(request: Request, env: Env) {
|
|
|
|
| 53 |
request: Request,
|
| 54 |
env: Env,
|
| 55 |
tokens: { refresh: string; expires_in: number; created_at: number },
|
| 56 |
+
identity?: Identity,
|
| 57 |
): Promise<ResponseInit> {
|
| 58 |
const { session, sessionStorage } = await getSession(request, env);
|
| 59 |
|
|
|
|
| 62 |
session.set('refresh', tokens.refresh);
|
| 63 |
session.set('expiresAt', expiresAt);
|
| 64 |
|
| 65 |
+
if (identity) {
|
| 66 |
+
session.set('userId', identity.userId ?? null);
|
| 67 |
+
session.set('segmentWriteKey', identity.segmentWriteKey ?? null);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
return {
|
| 71 |
headers: {
|
| 72 |
'Set-Cookie': await sessionStorage.commitSession(session, {
|
|
|
|
| 106 |
return jwtPayload.bolt === true;
|
| 107 |
}
|
| 108 |
|
| 109 |
+
export async function getSession(request: Request, env: Env) {
|
| 110 |
const sessionStorage = getSessionStorage(env);
|
| 111 |
const cookie = request.headers.get('Cookie');
|
| 112 |
|
packages/bolt/app/lib/analytics.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Analytics, type IdentifyParams, type PageParams, type TrackParams } from '@segment/analytics-node';
|
| 2 |
+
import { CLIENT_ORIGIN } from '~/lib/constants';
|
| 3 |
+
import { request as doRequest } from '~/lib/fetch';
|
| 4 |
+
import { logger } from '~/utils/logger';
|
| 5 |
+
|
| 6 |
+
export interface Identity {
|
| 7 |
+
userId?: string | null;
|
| 8 |
+
guestId?: string | null;
|
| 9 |
+
segmentWriteKey?: string | null;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const MESSAGE_PREFIX = 'Bolt';
|
| 13 |
+
|
| 14 |
+
export enum AnalyticsTrackEvent {
|
| 15 |
+
MessageSent = `${MESSAGE_PREFIX} Message Sent`,
|
| 16 |
+
ChatCreated = `${MESSAGE_PREFIX} Chat Created`,
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export enum AnalyticsAction {
|
| 20 |
+
Identify = 'identify',
|
| 21 |
+
Page = 'page',
|
| 22 |
+
Track = 'track',
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// we can omit the user ID since it's retrieved from the user's session
|
| 26 |
+
type OmitUserId<T> = Omit<T, 'userId'>;
|
| 27 |
+
|
| 28 |
+
export type AnalyticsEvent =
|
| 29 |
+
| { action: AnalyticsAction.Identify; payload: OmitUserId<IdentifyParams> }
|
| 30 |
+
| { action: AnalyticsAction.Page; payload: OmitUserId<PageParams> }
|
| 31 |
+
| { action: AnalyticsAction.Track; payload: OmitUserId<TrackParams> };
|
| 32 |
+
|
| 33 |
+
export async function identifyUser(access: string): Promise<Identity | undefined> {
|
| 34 |
+
const response = await doRequest(`${CLIENT_ORIGIN}/api/identify`, {
|
| 35 |
+
method: 'GET',
|
| 36 |
+
headers: { authorization: `Bearer ${access}` },
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
const body = await response.json();
|
| 40 |
+
|
| 41 |
+
if (!response.ok) {
|
| 42 |
+
return undefined;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// convert numerical identity values to strings
|
| 46 |
+
const stringified = Object.entries(body).map(([key, value]) => [
|
| 47 |
+
key,
|
| 48 |
+
typeof value === 'number' ? value.toString() : value,
|
| 49 |
+
]);
|
| 50 |
+
|
| 51 |
+
return Object.fromEntries(stringified) as Identity;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// send an analytics event from the client
|
| 55 |
+
export async function sendAnalyticsEvent(event: AnalyticsEvent) {
|
| 56 |
+
// don't send analytics events when in dev mode
|
| 57 |
+
if (import.meta.env.DEV) {
|
| 58 |
+
return;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
const request = await fetch('/api/analytics', {
|
| 62 |
+
method: 'POST',
|
| 63 |
+
headers: {
|
| 64 |
+
'Content-Type': 'application/json',
|
| 65 |
+
},
|
| 66 |
+
body: JSON.stringify(event),
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
if (!request.ok) {
|
| 70 |
+
logger.error(`Error handling Segment Analytics action: ${event.action}`);
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// send an analytics event from the server
|
| 75 |
+
export async function sendEventInternal(identity: Identity, { action, payload }: AnalyticsEvent) {
|
| 76 |
+
const { userId, segmentWriteKey: writeKey } = identity;
|
| 77 |
+
|
| 78 |
+
if (!userId || !writeKey) {
|
| 79 |
+
logger.warn('Missing user ID or write key when logging analytics');
|
| 80 |
+
return { success: false as const, error: 'missing-data' };
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
const analytics = new Analytics({ flushAt: 1, writeKey }).on('error', logger.error);
|
| 84 |
+
|
| 85 |
+
try {
|
| 86 |
+
await new Promise((resolve, reject) => {
|
| 87 |
+
if (action === AnalyticsAction.Identify) {
|
| 88 |
+
analytics.identify({ ...payload, userId }, resolve);
|
| 89 |
+
} else if (action === AnalyticsAction.Page) {
|
| 90 |
+
analytics.page({ ...payload, userId }, resolve);
|
| 91 |
+
} else if (action === AnalyticsAction.Track) {
|
| 92 |
+
analytics.track({ ...payload, userId }, resolve);
|
| 93 |
+
} else {
|
| 94 |
+
reject();
|
| 95 |
+
}
|
| 96 |
+
});
|
| 97 |
+
} catch {
|
| 98 |
+
logger.error(`Error handling Segment Analytics action: ${action}`);
|
| 99 |
+
return { success: false as const, error: 'invalid-action' };
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
return { success: true as const };
|
| 103 |
+
}
|
packages/bolt/app/lib/persistence/useChatHistory.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Message } from 'ai';
|
|
| 4 |
import { openDatabase, setMessages, getMessages, getNextId, getUrlId } from './db';
|
| 5 |
import { toast } from 'react-toastify';
|
| 6 |
import { workbenchStore } from '~/lib/stores/workbench';
|
|
|
|
| 7 |
|
| 8 |
export interface ChatHistory {
|
| 9 |
id: string;
|
|
@@ -111,4 +112,14 @@ function navigateChat(nextId: string) {
|
|
| 111 |
url.pathname = `/chat/${nextId}`;
|
| 112 |
|
| 113 |
window.history.replaceState({}, '', url);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
}
|
|
|
|
| 4 |
import { openDatabase, setMessages, getMessages, getNextId, getUrlId } from './db';
|
| 5 |
import { toast } from 'react-toastify';
|
| 6 |
import { workbenchStore } from '~/lib/stores/workbench';
|
| 7 |
+
import { sendAnalyticsEvent, AnalyticsAction } from '~/lib/analytics';
|
| 8 |
|
| 9 |
export interface ChatHistory {
|
| 10 |
id: string;
|
|
|
|
| 112 |
url.pathname = `/chat/${nextId}`;
|
| 113 |
|
| 114 |
window.history.replaceState({}, '', url);
|
| 115 |
+
|
| 116 |
+
// since the `replaceState` call doesn't trigger a page reload, we need to manually log this event
|
| 117 |
+
sendAnalyticsEvent({
|
| 118 |
+
action: AnalyticsAction.Page,
|
| 119 |
+
payload: {
|
| 120 |
+
properties: {
|
| 121 |
+
url: url.href,
|
| 122 |
+
},
|
| 123 |
+
},
|
| 124 |
+
});
|
| 125 |
}
|
packages/bolt/app/root.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
| 1 |
import { useStore } from '@nanostores/react';
|
| 2 |
import type { LinksFunction } from '@remix-run/cloudflare';
|
| 3 |
-
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';
|
| 4 |
import tailwindReset from '@unocss/reset/tailwind-compat.css?url';
|
|
|
|
|
|
|
| 5 |
import { themeStore } from './lib/stores/theme';
|
| 6 |
import { stripIndents } from './utils/stripIndent';
|
| 7 |
|
|
@@ -53,6 +55,20 @@ const inlineThemeCode = stripIndents`
|
|
| 53 |
export function Layout({ children }: { children: React.ReactNode }) {
|
| 54 |
const theme = useStore(themeStore);
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
return (
|
| 57 |
<html lang="en" data-theme={theme}>
|
| 58 |
<head>
|
|
|
|
| 1 |
import { useStore } from '@nanostores/react';
|
| 2 |
import type { LinksFunction } from '@remix-run/cloudflare';
|
| 3 |
+
import { Links, Meta, Outlet, Scripts, ScrollRestoration, useLocation } from '@remix-run/react';
|
| 4 |
import tailwindReset from '@unocss/reset/tailwind-compat.css?url';
|
| 5 |
+
import { useEffect } from 'react';
|
| 6 |
+
import { sendAnalyticsEvent, AnalyticsAction } from './lib/analytics';
|
| 7 |
import { themeStore } from './lib/stores/theme';
|
| 8 |
import { stripIndents } from './utils/stripIndent';
|
| 9 |
|
|
|
|
| 55 |
export function Layout({ children }: { children: React.ReactNode }) {
|
| 56 |
const theme = useStore(themeStore);
|
| 57 |
|
| 58 |
+
const { pathname } = useLocation();
|
| 59 |
+
|
| 60 |
+
// log page events when the window location changes
|
| 61 |
+
useEffect(() => {
|
| 62 |
+
sendAnalyticsEvent({
|
| 63 |
+
action: AnalyticsAction.Page,
|
| 64 |
+
payload: {
|
| 65 |
+
properties: {
|
| 66 |
+
url: window.location.href,
|
| 67 |
+
},
|
| 68 |
+
},
|
| 69 |
+
});
|
| 70 |
+
}, [pathname]);
|
| 71 |
+
|
| 72 |
return (
|
| 73 |
<html lang="en" data-theme={theme}>
|
| 74 |
<head>
|
packages/bolt/app/routes/api.analytics.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { json, type ActionFunctionArgs } from '@remix-run/cloudflare';
|
| 2 |
+
import { handleWithAuth } from '~/lib/.server/login';
|
| 3 |
+
import { getSession } from '~/lib/.server/sessions';
|
| 4 |
+
import { sendEventInternal, type AnalyticsEvent } from '~/lib/analytics';
|
| 5 |
+
|
| 6 |
+
async function analyticsAction({ request, context }: ActionFunctionArgs) {
|
| 7 |
+
const event: AnalyticsEvent = await request.json();
|
| 8 |
+
const { session } = await getSession(request, context.cloudflare.env);
|
| 9 |
+
const { success, error } = await sendEventInternal(session.data, event);
|
| 10 |
+
|
| 11 |
+
if (!success) {
|
| 12 |
+
return json({ error }, { status: 500 });
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
return json({ success }, { status: 200 });
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export async function action(args: ActionFunctionArgs) {
|
| 19 |
+
return handleWithAuth(args, analyticsAction);
|
| 20 |
+
}
|
packages/bolt/app/routes/login.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { useFetcher, useLoaderData } from '@remix-run/react';
|
|
| 9 |
import { useEffect, useState } from 'react';
|
| 10 |
import { LoadingDots } from '~/components/ui/LoadingDots';
|
| 11 |
import { createUserSession, isAuthenticated, validateAccessToken } from '~/lib/.server/sessions';
|
|
|
|
| 12 |
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
|
| 13 |
import { request as doRequest } from '~/lib/fetch';
|
| 14 |
import { auth, type AuthAPI } from '~/lib/webcontainer/auth.client';
|
|
@@ -62,9 +63,11 @@ export async function action({ request, context }: ActionFunctionArgs) {
|
|
| 62 |
return json({ error: 'bolt-access' as const }, { status: 401 });
|
| 63 |
}
|
| 64 |
|
|
|
|
|
|
|
| 65 |
const tokenInfo: { expires_in: number; created_at: number } = await response.json();
|
| 66 |
|
| 67 |
-
const init = await createUserSession(request, context.cloudflare.env, { ...payload, ...tokenInfo });
|
| 68 |
|
| 69 |
return redirectDocument('/', init);
|
| 70 |
}
|
|
@@ -105,6 +108,9 @@ export default function Login() {
|
|
| 105 |
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
|
| 106 |
</div>
|
| 107 |
<LoginForm />
|
|
|
|
|
|
|
|
|
|
| 108 |
</div>
|
| 109 |
)}
|
| 110 |
</div>
|
|
@@ -146,7 +152,7 @@ function LoginForm() {
|
|
| 146 |
});
|
| 147 |
}
|
| 148 |
|
| 149 |
-
function onTokens() {
|
| 150 |
const tokens = auth.tokens()!;
|
| 151 |
|
| 152 |
fetcher.submit(tokens, {
|
|
|
|
| 9 |
import { useEffect, useState } from 'react';
|
| 10 |
import { LoadingDots } from '~/components/ui/LoadingDots';
|
| 11 |
import { createUserSession, isAuthenticated, validateAccessToken } from '~/lib/.server/sessions';
|
| 12 |
+
import { identifyUser } from '~/lib/analytics';
|
| 13 |
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
|
| 14 |
import { request as doRequest } from '~/lib/fetch';
|
| 15 |
import { auth, type AuthAPI } from '~/lib/webcontainer/auth.client';
|
|
|
|
| 63 |
return json({ error: 'bolt-access' as const }, { status: 401 });
|
| 64 |
}
|
| 65 |
|
| 66 |
+
const identity = await identifyUser(payload.access);
|
| 67 |
+
|
| 68 |
const tokenInfo: { expires_in: number; created_at: number } = await response.json();
|
| 69 |
|
| 70 |
+
const init = await createUserSession(request, context.cloudflare.env, { ...payload, ...tokenInfo }, identity);
|
| 71 |
|
| 72 |
return redirectDocument('/', init);
|
| 73 |
}
|
|
|
|
| 108 |
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
|
| 109 |
</div>
|
| 110 |
<LoginForm />
|
| 111 |
+
<p className="mt-4 text-sm text-center text-gray-600">
|
| 112 |
+
By using Bolt, you agree to the collection of usage data for analytics.
|
| 113 |
+
</p>
|
| 114 |
</div>
|
| 115 |
)}
|
| 116 |
</div>
|
|
|
|
| 152 |
});
|
| 153 |
}
|
| 154 |
|
| 155 |
+
async function onTokens() {
|
| 156 |
const tokens = auth.tokens()!;
|
| 157 |
|
| 158 |
fetcher.submit(tokens, {
|
packages/bolt/package.json
CHANGED
|
@@ -39,6 +39,7 @@
|
|
| 39 |
"@remix-run/cloudflare": "^2.10.2",
|
| 40 |
"@remix-run/cloudflare-pages": "^2.10.2",
|
| 41 |
"@remix-run/react": "^2.10.2",
|
|
|
|
| 42 |
"@stackblitz/sdk": "^1.11.0",
|
| 43 |
"@uiw/codemirror-theme-vscode": "^4.23.0",
|
| 44 |
"@unocss/reset": "^0.61.0",
|
|
|
|
| 39 |
"@remix-run/cloudflare": "^2.10.2",
|
| 40 |
"@remix-run/cloudflare-pages": "^2.10.2",
|
| 41 |
"@remix-run/react": "^2.10.2",
|
| 42 |
+
"@segment/analytics-node": "^2.1.2",
|
| 43 |
"@stackblitz/sdk": "^1.11.0",
|
| 44 |
"@uiw/codemirror-theme-vscode": "^4.23.0",
|
| 45 |
"@unocss/reset": "^0.61.0",
|
pnpm-lock.yaml
CHANGED
|
@@ -104,6 +104,9 @@ importers:
|
|
| 104 |
'@remix-run/react':
|
| 105 |
specifier: ^2.10.2
|
| 106 |
version: 2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2)
|
|
|
|
|
|
|
|
|
|
| 107 |
'@stackblitz/sdk':
|
| 108 |
specifier: ^1.11.0
|
| 109 |
version: 1.11.0
|
|
@@ -1167,6 +1170,14 @@ packages:
|
|
| 1167 |
'@lezer/sass@1.0.6':
|
| 1168 |
resolution: {integrity: sha512-w/RCO2dIzZH1To8p+xjs8cE+yfgGus8NZ/dXeWl/QzHyr+TeBs71qiE70KPImEwvTsmEjoWh0A5SxMzKd5BWBQ==}
|
| 1169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1170 |
'@mdx-js/mdx@2.3.0':
|
| 1171 |
resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==}
|
| 1172 |
|
|
@@ -1444,6 +1455,16 @@ packages:
|
|
| 1444 |
cpu: [x64]
|
| 1445 |
os: [win32]
|
| 1446 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1447 |
'@shikijs/core@1.9.1':
|
| 1448 |
resolution: {integrity: sha512-EmUful2MQtY8KgCF1OkBtOuMcvaZEvmdubhW0UHCGXi21O9dRLeADVCj+k6ZS+de7Mz9d2qixOXJ+GLhcK3pXg==}
|
| 1449 |
|
|
@@ -1988,6 +2009,9 @@ packages:
|
|
| 1988 |
buffer@5.7.1:
|
| 1989 |
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
| 1990 |
|
|
|
|
|
|
|
|
|
|
| 1991 |
builtin-status-codes@3.0.0:
|
| 1992 |
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==}
|
| 1993 |
|
|
@@ -2381,6 +2405,10 @@ packages:
|
|
| 2381 |
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
|
| 2382 |
engines: {node: '>=12'}
|
| 2383 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2384 |
duplexer@0.1.2:
|
| 2385 |
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
| 2386 |
|
|
@@ -3742,6 +3770,15 @@ packages:
|
|
| 3742 |
node-fetch-native@1.6.4:
|
| 3743 |
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
|
| 3744 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3745 |
node-fetch@3.3.2:
|
| 3746 |
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
| 3747 |
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
|
@@ -4661,6 +4698,9 @@ packages:
|
|
| 4661 |
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
| 4662 |
engines: {node: '>=6'}
|
| 4663 |
|
|
|
|
|
|
|
|
|
|
| 4664 |
trim-lines@3.0.1:
|
| 4665 |
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
| 4666 |
|
|
@@ -4992,6 +5032,12 @@ packages:
|
|
| 4992 |
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
| 4993 |
engines: {node: '>= 8'}
|
| 4994 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4995 |
which-typed-array@1.1.15:
|
| 4996 |
resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==}
|
| 4997 |
engines: {node: '>= 0.4'}
|
|
@@ -6073,6 +6119,12 @@ snapshots:
|
|
| 6073 |
'@lezer/highlight': 1.2.0
|
| 6074 |
'@lezer/lr': 1.4.1
|
| 6075 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6076 |
'@mdx-js/mdx@2.3.0':
|
| 6077 |
dependencies:
|
| 6078 |
'@types/estree-jsx': 1.0.5
|
|
@@ -6425,6 +6477,29 @@ snapshots:
|
|
| 6425 |
'@rollup/rollup-win32-x64-msvc@4.18.0':
|
| 6426 |
optional: true
|
| 6427 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6428 |
'@shikijs/core@1.9.1': {}
|
| 6429 |
|
| 6430 |
'@sinclair/typebox@0.27.8': {}
|
|
@@ -7186,6 +7261,11 @@ snapshots:
|
|
| 7186 |
base64-js: 1.5.1
|
| 7187 |
ieee754: 1.2.1
|
| 7188 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7189 |
builtin-status-codes@3.0.0: {}
|
| 7190 |
|
| 7191 |
bytes@3.0.0:
|
|
@@ -7569,6 +7649,8 @@ snapshots:
|
|
| 7569 |
|
| 7570 |
dotenv@16.4.5: {}
|
| 7571 |
|
|
|
|
|
|
|
| 7572 |
duplexer@0.1.2: {}
|
| 7573 |
|
| 7574 |
duplexify@3.7.1:
|
|
@@ -9422,6 +9504,10 @@ snapshots:
|
|
| 9422 |
|
| 9423 |
node-fetch-native@1.6.4: {}
|
| 9424 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9425 |
node-fetch@3.3.2:
|
| 9426 |
dependencies:
|
| 9427 |
data-uri-to-buffer: 4.0.1
|
|
@@ -10441,6 +10527,8 @@ snapshots:
|
|
| 10441 |
|
| 10442 |
totalist@3.0.1: {}
|
| 10443 |
|
|
|
|
|
|
|
| 10444 |
trim-lines@3.0.1: {}
|
| 10445 |
|
| 10446 |
trough@2.2.0: {}
|
|
@@ -10842,6 +10930,13 @@ snapshots:
|
|
| 10842 |
|
| 10843 |
web-streams-polyfill@3.3.3: {}
|
| 10844 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10845 |
which-typed-array@1.1.15:
|
| 10846 |
dependencies:
|
| 10847 |
available-typed-arrays: 1.0.7
|
|
|
|
| 104 |
'@remix-run/react':
|
| 105 |
specifier: ^2.10.2
|
| 106 |
version: 2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2)
|
| 107 |
+
'@segment/analytics-node':
|
| 108 |
+
specifier: ^2.1.2
|
| 109 |
+
version: 2.1.2
|
| 110 |
'@stackblitz/sdk':
|
| 111 |
specifier: ^1.11.0
|
| 112 |
version: 1.11.0
|
|
|
|
| 1170 |
'@lezer/sass@1.0.6':
|
| 1171 |
resolution: {integrity: sha512-w/RCO2dIzZH1To8p+xjs8cE+yfgGus8NZ/dXeWl/QzHyr+TeBs71qiE70KPImEwvTsmEjoWh0A5SxMzKd5BWBQ==}
|
| 1172 |
|
| 1173 |
+
'@lukeed/csprng@1.1.0':
|
| 1174 |
+
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
| 1175 |
+
engines: {node: '>=8'}
|
| 1176 |
+
|
| 1177 |
+
'@lukeed/uuid@2.0.1':
|
| 1178 |
+
resolution: {integrity: sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==}
|
| 1179 |
+
engines: {node: '>=8'}
|
| 1180 |
+
|
| 1181 |
'@mdx-js/mdx@2.3.0':
|
| 1182 |
resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==}
|
| 1183 |
|
|
|
|
| 1455 |
cpu: [x64]
|
| 1456 |
os: [win32]
|
| 1457 |
|
| 1458 |
+
'@segment/analytics-core@1.6.0':
|
| 1459 |
+
resolution: {integrity: sha512-bn9X++IScUfpT7aJGjKU/yJAu/Ko2sYD6HsKA70Z2560E89x30pqgqboVKY8kootvQnT4UKCJiUr5NDMgjmWdQ==}
|
| 1460 |
+
|
| 1461 |
+
'@segment/analytics-generic-utils@1.2.0':
|
| 1462 |
+
resolution: {integrity: sha512-DfnW6mW3YQOLlDQQdR89k4EqfHb0g/3XvBXkovH1FstUN93eL1kfW9CsDcVQyH3bAC5ZsFyjA/o/1Q2j0QeoWw==}
|
| 1463 |
+
|
| 1464 |
+
'@segment/analytics-node@2.1.2':
|
| 1465 |
+
resolution: {integrity: sha512-CIqWH5G0pB/LAFAZEZtntAxujiYIpdk0F+YGhfM6N/qt4/VLWjFcd4VZXVLW7xqaxig64UKWGQhe8bszXDRXXw==}
|
| 1466 |
+
engines: {node: '>=18'}
|
| 1467 |
+
|
| 1468 |
'@shikijs/core@1.9.1':
|
| 1469 |
resolution: {integrity: sha512-EmUful2MQtY8KgCF1OkBtOuMcvaZEvmdubhW0UHCGXi21O9dRLeADVCj+k6ZS+de7Mz9d2qixOXJ+GLhcK3pXg==}
|
| 1470 |
|
|
|
|
| 2009 |
buffer@5.7.1:
|
| 2010 |
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
| 2011 |
|
| 2012 |
+
buffer@6.0.3:
|
| 2013 |
+
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
| 2014 |
+
|
| 2015 |
builtin-status-codes@3.0.0:
|
| 2016 |
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==}
|
| 2017 |
|
|
|
|
| 2405 |
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
|
| 2406 |
engines: {node: '>=12'}
|
| 2407 |
|
| 2408 |
+
dset@3.1.3:
|
| 2409 |
+
resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==}
|
| 2410 |
+
engines: {node: '>=4'}
|
| 2411 |
+
|
| 2412 |
duplexer@0.1.2:
|
| 2413 |
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
| 2414 |
|
|
|
|
| 3770 |
node-fetch-native@1.6.4:
|
| 3771 |
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
|
| 3772 |
|
| 3773 |
+
node-fetch@2.7.0:
|
| 3774 |
+
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
| 3775 |
+
engines: {node: 4.x || >=6.0.0}
|
| 3776 |
+
peerDependencies:
|
| 3777 |
+
encoding: ^0.1.0
|
| 3778 |
+
peerDependenciesMeta:
|
| 3779 |
+
encoding:
|
| 3780 |
+
optional: true
|
| 3781 |
+
|
| 3782 |
node-fetch@3.3.2:
|
| 3783 |
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
| 3784 |
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
|
|
|
| 4698 |
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
| 4699 |
engines: {node: '>=6'}
|
| 4700 |
|
| 4701 |
+
tr46@0.0.3:
|
| 4702 |
+
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
| 4703 |
+
|
| 4704 |
trim-lines@3.0.1:
|
| 4705 |
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
| 4706 |
|
|
|
|
| 5032 |
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
| 5033 |
engines: {node: '>= 8'}
|
| 5034 |
|
| 5035 |
+
webidl-conversions@3.0.1:
|
| 5036 |
+
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
| 5037 |
+
|
| 5038 |
+
whatwg-url@5.0.0:
|
| 5039 |
+
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
| 5040 |
+
|
| 5041 |
which-typed-array@1.1.15:
|
| 5042 |
resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==}
|
| 5043 |
engines: {node: '>= 0.4'}
|
|
|
|
| 6119 |
'@lezer/highlight': 1.2.0
|
| 6120 |
'@lezer/lr': 1.4.1
|
| 6121 |
|
| 6122 |
+
'@lukeed/csprng@1.1.0': {}
|
| 6123 |
+
|
| 6124 |
+
'@lukeed/uuid@2.0.1':
|
| 6125 |
+
dependencies:
|
| 6126 |
+
'@lukeed/csprng': 1.1.0
|
| 6127 |
+
|
| 6128 |
'@mdx-js/mdx@2.3.0':
|
| 6129 |
dependencies:
|
| 6130 |
'@types/estree-jsx': 1.0.5
|
|
|
|
| 6477 |
'@rollup/rollup-win32-x64-msvc@4.18.0':
|
| 6478 |
optional: true
|
| 6479 |
|
| 6480 |
+
'@segment/analytics-core@1.6.0':
|
| 6481 |
+
dependencies:
|
| 6482 |
+
'@lukeed/uuid': 2.0.1
|
| 6483 |
+
'@segment/analytics-generic-utils': 1.2.0
|
| 6484 |
+
dset: 3.1.3
|
| 6485 |
+
tslib: 2.6.3
|
| 6486 |
+
|
| 6487 |
+
'@segment/analytics-generic-utils@1.2.0':
|
| 6488 |
+
dependencies:
|
| 6489 |
+
tslib: 2.6.3
|
| 6490 |
+
|
| 6491 |
+
'@segment/analytics-node@2.1.2':
|
| 6492 |
+
dependencies:
|
| 6493 |
+
'@lukeed/uuid': 2.0.1
|
| 6494 |
+
'@segment/analytics-core': 1.6.0
|
| 6495 |
+
'@segment/analytics-generic-utils': 1.2.0
|
| 6496 |
+
buffer: 6.0.3
|
| 6497 |
+
jose: 5.6.3
|
| 6498 |
+
node-fetch: 2.7.0
|
| 6499 |
+
tslib: 2.6.3
|
| 6500 |
+
transitivePeerDependencies:
|
| 6501 |
+
- encoding
|
| 6502 |
+
|
| 6503 |
'@shikijs/core@1.9.1': {}
|
| 6504 |
|
| 6505 |
'@sinclair/typebox@0.27.8': {}
|
|
|
|
| 7261 |
base64-js: 1.5.1
|
| 7262 |
ieee754: 1.2.1
|
| 7263 |
|
| 7264 |
+
buffer@6.0.3:
|
| 7265 |
+
dependencies:
|
| 7266 |
+
base64-js: 1.5.1
|
| 7267 |
+
ieee754: 1.2.1
|
| 7268 |
+
|
| 7269 |
builtin-status-codes@3.0.0: {}
|
| 7270 |
|
| 7271 |
bytes@3.0.0:
|
|
|
|
| 7649 |
|
| 7650 |
dotenv@16.4.5: {}
|
| 7651 |
|
| 7652 |
+
dset@3.1.3: {}
|
| 7653 |
+
|
| 7654 |
duplexer@0.1.2: {}
|
| 7655 |
|
| 7656 |
duplexify@3.7.1:
|
|
|
|
| 9504 |
|
| 9505 |
node-fetch-native@1.6.4: {}
|
| 9506 |
|
| 9507 |
+
node-fetch@2.7.0:
|
| 9508 |
+
dependencies:
|
| 9509 |
+
whatwg-url: 5.0.0
|
| 9510 |
+
|
| 9511 |
node-fetch@3.3.2:
|
| 9512 |
dependencies:
|
| 9513 |
data-uri-to-buffer: 4.0.1
|
|
|
|
| 10527 |
|
| 10528 |
totalist@3.0.1: {}
|
| 10529 |
|
| 10530 |
+
tr46@0.0.3: {}
|
| 10531 |
+
|
| 10532 |
trim-lines@3.0.1: {}
|
| 10533 |
|
| 10534 |
trough@2.2.0: {}
|
|
|
|
| 10930 |
|
| 10931 |
web-streams-polyfill@3.3.3: {}
|
| 10932 |
|
| 10933 |
+
webidl-conversions@3.0.1: {}
|
| 10934 |
+
|
| 10935 |
+
whatwg-url@5.0.0:
|
| 10936 |
+
dependencies:
|
| 10937 |
+
tr46: 0.0.3
|
| 10938 |
+
webidl-conversions: 3.0.1
|
| 10939 |
+
|
| 10940 |
which-typed-array@1.1.15:
|
| 10941 |
dependencies:
|
| 10942 |
available-typed-arrays: 1.0.7
|