Connor Fogarty commited on
Commit
8fd9d44
·
unverified ·
1 Parent(s): 6e99e4c

feat: add basic analytics (#29)

Browse files
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
 
 
 
107
  '@stackblitz/sdk':
108
  specifier: ^1.11.0
109
  version: 1.11.0
@@ -1167,6 +1170,14 @@ packages:
1167
  '@lezer/[email protected]':
1168
  resolution: {integrity: sha512-w/RCO2dIzZH1To8p+xjs8cE+yfgGus8NZ/dXeWl/QzHyr+TeBs71qiE70KPImEwvTsmEjoWh0A5SxMzKd5BWBQ==}
1169
 
 
 
 
 
 
 
 
 
1170
  '@mdx-js/[email protected]':
1171
  resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==}
1172
 
@@ -1444,6 +1455,16 @@ packages:
1444
  cpu: [x64]
1445
  os: [win32]
1446
 
 
 
 
 
 
 
 
 
 
 
1447
  '@shikijs/[email protected]':
1448
  resolution: {integrity: sha512-EmUful2MQtY8KgCF1OkBtOuMcvaZEvmdubhW0UHCGXi21O9dRLeADVCj+k6ZS+de7Mz9d2qixOXJ+GLhcK3pXg==}
1449
 
@@ -1988,6 +2009,9 @@ packages:
1988
1989
  resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
1990
 
 
 
 
1991
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
2385
  resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
2386
 
@@ -3742,6 +3770,15 @@ packages:
3742
3743
  resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
3744
 
 
 
 
 
 
 
 
 
 
3745
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
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
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/[email protected]':
6077
  dependencies:
6078
  '@types/estree-jsx': 1.0.5
@@ -6425,6 +6477,29 @@ snapshots:
6425
  '@rollup/[email protected]':
6426
  optional: true
6427
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6428
  '@shikijs/[email protected]': {}
6429
 
6430
  '@sinclair/[email protected]': {}
@@ -7186,6 +7261,11 @@ snapshots:
7186
  base64-js: 1.5.1
7187
  ieee754: 1.2.1
7188
 
 
 
 
 
 
7189
7190
 
7191
@@ -7569,6 +7649,8 @@ snapshots:
7569
 
7570
7571
 
 
 
7572
7573
 
7574
@@ -9422,6 +9504,10 @@ snapshots:
9422
 
9423
9424
 
 
 
 
 
9425
9426
  dependencies:
9427
  data-uri-to-buffer: 4.0.1
@@ -10441,6 +10527,8 @@ snapshots:
10441
 
10442
10443
 
 
 
10444
10445
 
10446
@@ -10842,6 +10930,13 @@ snapshots:
10842
 
10843
10844
 
 
 
 
 
 
 
 
10845
10846
  dependencies:
10847
  available-typed-arrays: 1.0.7
 
104
  '@remix-run/react':
105
  specifier: ^2.10.2
106
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/[email protected]':
1171
  resolution: {integrity: sha512-w/RCO2dIzZH1To8p+xjs8cE+yfgGus8NZ/dXeWl/QzHyr+TeBs71qiE70KPImEwvTsmEjoWh0A5SxMzKd5BWBQ==}
1172
 
1173
+ '@lukeed/[email protected]':
1174
+ resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
1175
+ engines: {node: '>=8'}
1176
+
1177
+ '@lukeed/[email protected]':
1178
+ resolution: {integrity: sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==}
1179
+ engines: {node: '>=8'}
1180
+
1181
  '@mdx-js/[email protected]':
1182
  resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==}
1183
 
 
1455
  cpu: [x64]
1456
  os: [win32]
1457
 
1458
+ '@segment/[email protected]':
1459
+ resolution: {integrity: sha512-bn9X++IScUfpT7aJGjKU/yJAu/Ko2sYD6HsKA70Z2560E89x30pqgqboVKY8kootvQnT4UKCJiUr5NDMgjmWdQ==}
1460
+
1461
+ '@segment/[email protected]':
1462
+ resolution: {integrity: sha512-DfnW6mW3YQOLlDQQdR89k4EqfHb0g/3XvBXkovH1FstUN93eL1kfW9CsDcVQyH3bAC5ZsFyjA/o/1Q2j0QeoWw==}
1463
+
1464
+ '@segment/[email protected]':
1465
+ resolution: {integrity: sha512-CIqWH5G0pB/LAFAZEZtntAxujiYIpdk0F+YGhfM6N/qt4/VLWjFcd4VZXVLW7xqaxig64UKWGQhe8bszXDRXXw==}
1466
+ engines: {node: '>=18'}
1467
+
1468
  '@shikijs/[email protected]':
1469
  resolution: {integrity: sha512-EmUful2MQtY8KgCF1OkBtOuMcvaZEvmdubhW0UHCGXi21O9dRLeADVCj+k6ZS+de7Mz9d2qixOXJ+GLhcK3pXg==}
1470
 
 
2009
2010
  resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
2011
 
2012
2013
+ resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
2014
+
2015
2016
  resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==}
2017
 
 
2405
  resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
2406
  engines: {node: '>=12'}
2407
 
2408
2409
+ resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==}
2410
+ engines: {node: '>=4'}
2411
+
2412
2413
  resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
2414
 
 
3770
3771
  resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
3772
 
3773
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
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
4702
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
4703
+
4704
4705
  resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
4706
 
 
5032
  resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
5033
  engines: {node: '>= 8'}
5034
 
5035
5036
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
5037
+
5038
5039
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
5040
+
5041
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/[email protected]': {}
6123
+
6124
+ '@lukeed/[email protected]':
6125
+ dependencies:
6126
+ '@lukeed/csprng': 1.1.0
6127
+
6128
  '@mdx-js/[email protected]':
6129
  dependencies:
6130
  '@types/estree-jsx': 1.0.5
 
6477
  '@rollup/[email protected]':
6478
  optional: true
6479
 
6480
+ '@segment/[email protected]':
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/[email protected]':
6488
+ dependencies:
6489
+ tslib: 2.6.3
6490
+
6491
+ '@segment/[email protected]':
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/[email protected]': {}
6504
 
6505
  '@sinclair/[email protected]': {}
 
7261
  base64-js: 1.5.1
7262
  ieee754: 1.2.1
7263
 
7264
7265
+ dependencies:
7266
+ base64-js: 1.5.1
7267
+ ieee754: 1.2.1
7268
+
7269
7270
 
7271
 
7649
 
7650
7651
 
7652
7653
+
7654
7655
 
7656
 
9504
 
9505
9506
 
9507
9508
+ dependencies:
9509
+ whatwg-url: 5.0.0
9510
+
9511
9512
  dependencies:
9513
  data-uri-to-buffer: 4.0.1
 
10527
 
10528
10529
 
10530
10531
+
10532
10533
 
10534
 
10930
 
10931
10932
 
10933
10934
+
10935
10936
+ dependencies:
10937
+ tr46: 0.0.3
10938
+ webidl-conversions: 3.0.1
10939
+
10940
10941
  dependencies:
10942
  available-typed-arrays: 1.0.7