feat: add basic analytics (#29)

This commit is contained in:
Connor Fogarty 2024-08-12 10:37:45 -05:00 committed by GitHub
parent 6e99e4c11e
commit 8fd9d4477e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 278 additions and 4 deletions

View File

@ -11,6 +11,7 @@ import { fileModificationsToHTML } from '~/utils/diff';
import { cubicEasingFn } from '~/utils/easings'; import { cubicEasingFn } from '~/utils/easings';
import { createScopedLogger, renderLogger } from '~/utils/logger'; import { createScopedLogger, renderLogger } from '~/utils/logger';
import { BaseChat } from './BaseChat'; import { BaseChat } from './BaseChat';
import { sendAnalyticsEvent, AnalyticsTrackEvent, AnalyticsAction } from '~/lib/analytics';
const toastAnimation = cssTransition({ const toastAnimation = cssTransition({
enter: 'animated fadeInRight', enter: 'animated fadeInRight',
@ -191,6 +192,18 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
resetEnhancer(); resetEnhancer();
textareaRef.current?.blur(); textareaRef.current?.blur();
const event = messages.length === 0 ? AnalyticsTrackEvent.ChatCreated : AnalyticsTrackEvent.MessageSent;
sendAnalyticsEvent({
action: AnalyticsAction.Track,
payload: {
event,
properties: {
message: _input,
},
},
});
}; };
const [messageRef, scrollRef] = useSnapScroll(); const [messageRef, scrollRef] = useSnapScroll();

View File

@ -3,12 +3,15 @@ import { decodeJwt } from 'jose';
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants'; import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
import { request as doRequest } from '~/lib/fetch'; import { request as doRequest } from '~/lib/fetch';
import { logger } from '~/utils/logger'; import { logger } from '~/utils/logger';
import type { Identity } from '~/lib/analytics';
const DEV_SESSION_SECRET = import.meta.env.DEV ? 'LZQMrERo3Ewn/AbpSYJ9aw==' : undefined; const DEV_SESSION_SECRET = import.meta.env.DEV ? 'LZQMrERo3Ewn/AbpSYJ9aw==' : undefined;
interface SessionData { interface SessionData {
refresh: string; refresh: string;
expiresAt: number; expiresAt: number;
userId: string | null;
segmentWriteKey: string | null;
} }
export async function isAuthenticated(request: Request, env: Env) { export async function isAuthenticated(request: Request, env: Env) {
@ -50,6 +53,7 @@ export async function createUserSession(
request: Request, request: Request,
env: Env, env: Env,
tokens: { refresh: string; expires_in: number; created_at: number }, tokens: { refresh: string; expires_in: number; created_at: number },
identity?: Identity,
): Promise<ResponseInit> { ): Promise<ResponseInit> {
const { session, sessionStorage } = await getSession(request, env); const { session, sessionStorage } = await getSession(request, env);
@ -58,6 +62,11 @@ export async function createUserSession(
session.set('refresh', tokens.refresh); session.set('refresh', tokens.refresh);
session.set('expiresAt', expiresAt); session.set('expiresAt', expiresAt);
if (identity) {
session.set('userId', identity.userId ?? null);
session.set('segmentWriteKey', identity.segmentWriteKey ?? null);
}
return { return {
headers: { headers: {
'Set-Cookie': await sessionStorage.commitSession(session, { 'Set-Cookie': await sessionStorage.commitSession(session, {
@ -97,7 +106,7 @@ export function validateAccessToken(access: string) {
return jwtPayload.bolt === true; return jwtPayload.bolt === true;
} }
async function getSession(request: Request, env: Env) { export async function getSession(request: Request, env: Env) {
const sessionStorage = getSessionStorage(env); const sessionStorage = getSessionStorage(env);
const cookie = request.headers.get('Cookie'); const cookie = request.headers.get('Cookie');

View File

@ -0,0 +1,103 @@
import { Analytics, type IdentifyParams, type PageParams, type TrackParams } from '@segment/analytics-node';
import { CLIENT_ORIGIN } from '~/lib/constants';
import { request as doRequest } from '~/lib/fetch';
import { logger } from '~/utils/logger';
export interface Identity {
userId?: string | null;
guestId?: string | null;
segmentWriteKey?: string | null;
}
const MESSAGE_PREFIX = 'Bolt';
export enum AnalyticsTrackEvent {
MessageSent = `${MESSAGE_PREFIX} Message Sent`,
ChatCreated = `${MESSAGE_PREFIX} Chat Created`,
}
export enum AnalyticsAction {
Identify = 'identify',
Page = 'page',
Track = 'track',
}
// we can omit the user ID since it's retrieved from the user's session
type OmitUserId<T> = Omit<T, 'userId'>;
export type AnalyticsEvent =
| { action: AnalyticsAction.Identify; payload: OmitUserId<IdentifyParams> }
| { action: AnalyticsAction.Page; payload: OmitUserId<PageParams> }
| { action: AnalyticsAction.Track; payload: OmitUserId<TrackParams> };
export async function identifyUser(access: string): Promise<Identity | undefined> {
const response = await doRequest(`${CLIENT_ORIGIN}/api/identify`, {
method: 'GET',
headers: { authorization: `Bearer ${access}` },
});
const body = await response.json();
if (!response.ok) {
return undefined;
}
// convert numerical identity values to strings
const stringified = Object.entries(body).map(([key, value]) => [
key,
typeof value === 'number' ? value.toString() : value,
]);
return Object.fromEntries(stringified) as Identity;
}
// send an analytics event from the client
export async function sendAnalyticsEvent(event: AnalyticsEvent) {
// don't send analytics events when in dev mode
if (import.meta.env.DEV) {
return;
}
const request = await fetch('/api/analytics', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(event),
});
if (!request.ok) {
logger.error(`Error handling Segment Analytics action: ${event.action}`);
}
}
// send an analytics event from the server
export async function sendEventInternal(identity: Identity, { action, payload }: AnalyticsEvent) {
const { userId, segmentWriteKey: writeKey } = identity;
if (!userId || !writeKey) {
logger.warn('Missing user ID or write key when logging analytics');
return { success: false as const, error: 'missing-data' };
}
const analytics = new Analytics({ flushAt: 1, writeKey }).on('error', logger.error);
try {
await new Promise((resolve, reject) => {
if (action === AnalyticsAction.Identify) {
analytics.identify({ ...payload, userId }, resolve);
} else if (action === AnalyticsAction.Page) {
analytics.page({ ...payload, userId }, resolve);
} else if (action === AnalyticsAction.Track) {
analytics.track({ ...payload, userId }, resolve);
} else {
reject();
}
});
} catch {
logger.error(`Error handling Segment Analytics action: ${action}`);
return { success: false as const, error: 'invalid-action' };
}
return { success: true as const };
}

View File

@ -4,6 +4,7 @@ import type { Message } from 'ai';
import { openDatabase, setMessages, getMessages, getNextId, getUrlId } from './db'; import { openDatabase, setMessages, getMessages, getNextId, getUrlId } from './db';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { workbenchStore } from '~/lib/stores/workbench'; import { workbenchStore } from '~/lib/stores/workbench';
import { sendAnalyticsEvent, AnalyticsAction } from '~/lib/analytics';
export interface ChatHistory { export interface ChatHistory {
id: string; id: string;
@ -111,4 +112,14 @@ function navigateChat(nextId: string) {
url.pathname = `/chat/${nextId}`; url.pathname = `/chat/${nextId}`;
window.history.replaceState({}, '', url); window.history.replaceState({}, '', url);
// since the `replaceState` call doesn't trigger a page reload, we need to manually log this event
sendAnalyticsEvent({
action: AnalyticsAction.Page,
payload: {
properties: {
url: url.href,
},
},
});
} }

View File

@ -1,7 +1,9 @@
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import type { LinksFunction } from '@remix-run/cloudflare'; import type { LinksFunction } from '@remix-run/cloudflare';
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react'; import { Links, Meta, Outlet, Scripts, ScrollRestoration, useLocation } from '@remix-run/react';
import tailwindReset from '@unocss/reset/tailwind-compat.css?url'; import tailwindReset from '@unocss/reset/tailwind-compat.css?url';
import { useEffect } from 'react';
import { sendAnalyticsEvent, AnalyticsAction } from './lib/analytics';
import { themeStore } from './lib/stores/theme'; import { themeStore } from './lib/stores/theme';
import { stripIndents } from './utils/stripIndent'; import { stripIndents } from './utils/stripIndent';
@ -53,6 +55,20 @@ const inlineThemeCode = stripIndents`
export function Layout({ children }: { children: React.ReactNode }) { export function Layout({ children }: { children: React.ReactNode }) {
const theme = useStore(themeStore); const theme = useStore(themeStore);
const { pathname } = useLocation();
// log page events when the window location changes
useEffect(() => {
sendAnalyticsEvent({
action: AnalyticsAction.Page,
payload: {
properties: {
url: window.location.href,
},
},
});
}, [pathname]);
return ( return (
<html lang="en" data-theme={theme}> <html lang="en" data-theme={theme}>
<head> <head>

View File

@ -0,0 +1,20 @@
import { json, type ActionFunctionArgs } from '@remix-run/cloudflare';
import { handleWithAuth } from '~/lib/.server/login';
import { getSession } from '~/lib/.server/sessions';
import { sendEventInternal, type AnalyticsEvent } from '~/lib/analytics';
async function analyticsAction({ request, context }: ActionFunctionArgs) {
const event: AnalyticsEvent = await request.json();
const { session } = await getSession(request, context.cloudflare.env);
const { success, error } = await sendEventInternal(session.data, event);
if (!success) {
return json({ error }, { status: 500 });
}
return json({ success }, { status: 200 });
}
export async function action(args: ActionFunctionArgs) {
return handleWithAuth(args, analyticsAction);
}

View File

@ -9,6 +9,7 @@ import { useFetcher, useLoaderData } from '@remix-run/react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { LoadingDots } from '~/components/ui/LoadingDots'; import { LoadingDots } from '~/components/ui/LoadingDots';
import { createUserSession, isAuthenticated, validateAccessToken } from '~/lib/.server/sessions'; import { createUserSession, isAuthenticated, validateAccessToken } from '~/lib/.server/sessions';
import { identifyUser } from '~/lib/analytics';
import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants'; import { CLIENT_ID, CLIENT_ORIGIN } from '~/lib/constants';
import { request as doRequest } from '~/lib/fetch'; import { request as doRequest } from '~/lib/fetch';
import { auth, type AuthAPI } from '~/lib/webcontainer/auth.client'; import { auth, type AuthAPI } from '~/lib/webcontainer/auth.client';
@ -62,9 +63,11 @@ export async function action({ request, context }: ActionFunctionArgs) {
return json({ error: 'bolt-access' as const }, { status: 401 }); return json({ error: 'bolt-access' as const }, { status: 401 });
} }
const identity = await identifyUser(payload.access);
const tokenInfo: { expires_in: number; created_at: number } = await response.json(); const tokenInfo: { expires_in: number; created_at: number } = await response.json();
const init = await createUserSession(request, context.cloudflare.env, { ...payload, ...tokenInfo }); const init = await createUserSession(request, context.cloudflare.env, { ...payload, ...tokenInfo }, identity);
return redirectDocument('/', init); return redirectDocument('/', init);
} }
@ -105,6 +108,9 @@ export default function Login() {
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2> <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
</div> </div>
<LoginForm /> <LoginForm />
<p className="mt-4 text-sm text-center text-gray-600">
By using Bolt, you agree to the collection of usage data for analytics.
</p>
</div> </div>
)} )}
</div> </div>
@ -146,7 +152,7 @@ function LoginForm() {
}); });
} }
function onTokens() { async function onTokens() {
const tokens = auth.tokens()!; const tokens = auth.tokens()!;
fetcher.submit(tokens, { fetcher.submit(tokens, {

View File

@ -39,6 +39,7 @@
"@remix-run/cloudflare": "^2.10.2", "@remix-run/cloudflare": "^2.10.2",
"@remix-run/cloudflare-pages": "^2.10.2", "@remix-run/cloudflare-pages": "^2.10.2",
"@remix-run/react": "^2.10.2", "@remix-run/react": "^2.10.2",
"@segment/analytics-node": "^2.1.2",
"@stackblitz/sdk": "^1.11.0", "@stackblitz/sdk": "^1.11.0",
"@uiw/codemirror-theme-vscode": "^4.23.0", "@uiw/codemirror-theme-vscode": "^4.23.0",
"@unocss/reset": "^0.61.0", "@unocss/reset": "^0.61.0",

View File

@ -104,6 +104,9 @@ importers:
'@remix-run/react': '@remix-run/react':
specifier: ^2.10.2 specifier: ^2.10.2
version: 2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2) version: 2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2)
'@segment/analytics-node':
specifier: ^2.1.2
version: 2.1.2
'@stackblitz/sdk': '@stackblitz/sdk':
specifier: ^1.11.0 specifier: ^1.11.0
version: 1.11.0 version: 1.11.0
@ -1167,6 +1170,14 @@ packages:
'@lezer/sass@1.0.6': '@lezer/sass@1.0.6':
resolution: {integrity: sha512-w/RCO2dIzZH1To8p+xjs8cE+yfgGus8NZ/dXeWl/QzHyr+TeBs71qiE70KPImEwvTsmEjoWh0A5SxMzKd5BWBQ==} resolution: {integrity: sha512-w/RCO2dIzZH1To8p+xjs8cE+yfgGus8NZ/dXeWl/QzHyr+TeBs71qiE70KPImEwvTsmEjoWh0A5SxMzKd5BWBQ==}
'@lukeed/csprng@1.1.0':
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
engines: {node: '>=8'}
'@lukeed/uuid@2.0.1':
resolution: {integrity: sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==}
engines: {node: '>=8'}
'@mdx-js/mdx@2.3.0': '@mdx-js/mdx@2.3.0':
resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==} resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==}
@ -1444,6 +1455,16 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@segment/analytics-core@1.6.0':
resolution: {integrity: sha512-bn9X++IScUfpT7aJGjKU/yJAu/Ko2sYD6HsKA70Z2560E89x30pqgqboVKY8kootvQnT4UKCJiUr5NDMgjmWdQ==}
'@segment/analytics-generic-utils@1.2.0':
resolution: {integrity: sha512-DfnW6mW3YQOLlDQQdR89k4EqfHb0g/3XvBXkovH1FstUN93eL1kfW9CsDcVQyH3bAC5ZsFyjA/o/1Q2j0QeoWw==}
'@segment/analytics-node@2.1.2':
resolution: {integrity: sha512-CIqWH5G0pB/LAFAZEZtntAxujiYIpdk0F+YGhfM6N/qt4/VLWjFcd4VZXVLW7xqaxig64UKWGQhe8bszXDRXXw==}
engines: {node: '>=18'}
'@shikijs/core@1.9.1': '@shikijs/core@1.9.1':
resolution: {integrity: sha512-EmUful2MQtY8KgCF1OkBtOuMcvaZEvmdubhW0UHCGXi21O9dRLeADVCj+k6ZS+de7Mz9d2qixOXJ+GLhcK3pXg==} resolution: {integrity: sha512-EmUful2MQtY8KgCF1OkBtOuMcvaZEvmdubhW0UHCGXi21O9dRLeADVCj+k6ZS+de7Mz9d2qixOXJ+GLhcK3pXg==}
@ -1988,6 +2009,9 @@ packages:
buffer@5.7.1: buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
builtin-status-codes@3.0.0: builtin-status-codes@3.0.0:
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==}
@ -2381,6 +2405,10 @@ packages:
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
engines: {node: '>=12'} engines: {node: '>=12'}
dset@3.1.3:
resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==}
engines: {node: '>=4'}
duplexer@0.1.2: duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
@ -3742,6 +3770,15 @@ packages:
node-fetch-native@1.6.4: node-fetch-native@1.6.4:
resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-fetch@3.3.2: node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -4661,6 +4698,9 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
trim-lines@3.0.1: trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
@ -4992,6 +5032,12 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which-typed-array@1.1.15: which-typed-array@1.1.15:
resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -6073,6 +6119,12 @@ snapshots:
'@lezer/highlight': 1.2.0 '@lezer/highlight': 1.2.0
'@lezer/lr': 1.4.1 '@lezer/lr': 1.4.1
'@lukeed/csprng@1.1.0': {}
'@lukeed/uuid@2.0.1':
dependencies:
'@lukeed/csprng': 1.1.0
'@mdx-js/mdx@2.3.0': '@mdx-js/mdx@2.3.0':
dependencies: dependencies:
'@types/estree-jsx': 1.0.5 '@types/estree-jsx': 1.0.5
@ -6425,6 +6477,29 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.18.0': '@rollup/rollup-win32-x64-msvc@4.18.0':
optional: true optional: true
'@segment/analytics-core@1.6.0':
dependencies:
'@lukeed/uuid': 2.0.1
'@segment/analytics-generic-utils': 1.2.0
dset: 3.1.3
tslib: 2.6.3
'@segment/analytics-generic-utils@1.2.0':
dependencies:
tslib: 2.6.3
'@segment/analytics-node@2.1.2':
dependencies:
'@lukeed/uuid': 2.0.1
'@segment/analytics-core': 1.6.0
'@segment/analytics-generic-utils': 1.2.0
buffer: 6.0.3
jose: 5.6.3
node-fetch: 2.7.0
tslib: 2.6.3
transitivePeerDependencies:
- encoding
'@shikijs/core@1.9.1': {} '@shikijs/core@1.9.1': {}
'@sinclair/typebox@0.27.8': {} '@sinclair/typebox@0.27.8': {}
@ -7186,6 +7261,11 @@ snapshots:
base64-js: 1.5.1 base64-js: 1.5.1
ieee754: 1.2.1 ieee754: 1.2.1
buffer@6.0.3:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
builtin-status-codes@3.0.0: {} builtin-status-codes@3.0.0: {}
bytes@3.0.0: bytes@3.0.0:
@ -7569,6 +7649,8 @@ snapshots:
dotenv@16.4.5: {} dotenv@16.4.5: {}
dset@3.1.3: {}
duplexer@0.1.2: {} duplexer@0.1.2: {}
duplexify@3.7.1: duplexify@3.7.1:
@ -9422,6 +9504,10 @@ snapshots:
node-fetch-native@1.6.4: {} node-fetch-native@1.6.4: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-fetch@3.3.2: node-fetch@3.3.2:
dependencies: dependencies:
data-uri-to-buffer: 4.0.1 data-uri-to-buffer: 4.0.1
@ -10441,6 +10527,8 @@ snapshots:
totalist@3.0.1: {} totalist@3.0.1: {}
tr46@0.0.3: {}
trim-lines@3.0.1: {} trim-lines@3.0.1: {}
trough@2.2.0: {} trough@2.2.0: {}
@ -10842,6 +10930,13 @@ snapshots:
web-streams-polyfill@3.3.3: {} web-streams-polyfill@3.3.3: {}
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
which-typed-array@1.1.15: which-typed-array@1.1.15:
dependencies: dependencies:
available-typed-arrays: 1.0.7 available-typed-arrays: 1.0.7