@comergehq/studio 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,6 +16,7 @@ import { StudioOverlay } from './ui/StudioOverlay';
16
16
  import { LiquidGlassResetProvider } from '../components/utils/liquidGlassReset';
17
17
  import { useEditQueue } from './hooks/useEditQueue';
18
18
  import { useEditQueueActions } from './hooks/useEditQueueActions';
19
+ import { useAgentRunProgress } from './hooks/useAgentRunProgress';
19
20
  import { appsRepository } from '../data/apps/repository';
20
21
  import type { SyncUpstreamStatus } from '../data/apps/types';
21
22
 
@@ -23,9 +24,11 @@ export type ComergeStudioProps = {
23
24
  appId: string;
24
25
  clientKey: string;
25
26
  appKey?: string;
27
+ analyticsEnabled?: boolean;
26
28
  onNavigateHome?: () => void;
27
29
  style?: ViewStyle;
28
30
  showBubble?: boolean;
31
+ enableAgentProgress?: boolean;
29
32
  studioControlOptions?: import('@comergehq/studio-control').StudioControlOptions;
30
33
  embeddedBaseBundles?: EmbeddedBaseBundles;
31
34
  };
@@ -34,9 +37,11 @@ export function ComergeStudio({
34
37
  appId,
35
38
  clientKey,
36
39
  appKey = 'MicroMain',
40
+ analyticsEnabled,
37
41
  onNavigateHome,
38
42
  style,
39
43
  showBubble = true,
44
+ enableAgentProgress = true,
40
45
  studioControlOptions,
41
46
  embeddedBaseBundles,
42
47
  }: ComergeStudioProps) {
@@ -54,7 +59,11 @@ export function ComergeStudio({
54
59
  const captureTargetRef = React.useRef<View | null>(null);
55
60
 
56
61
  return (
57
- <StudioBootstrap clientKey={clientKey} fallback={<View style={{ flex: 1 }} />}>
62
+ <StudioBootstrap
63
+ clientKey={clientKey}
64
+ analyticsEnabled={analyticsEnabled}
65
+ fallback={<View style={{ flex: 1 }} />}
66
+ >
58
67
  {({ userId }) => (
59
68
  <BottomSheetModalProvider>
60
69
  <LiquidGlassResetProvider resetTriggers={[appId, activeAppId, runtimeAppId]}>
@@ -72,6 +81,7 @@ export function ComergeStudio({
72
81
  captureTargetRef={captureTargetRef}
73
82
  style={style}
74
83
  showBubble={showBubble}
84
+ enableAgentProgress={enableAgentProgress}
75
85
  studioControlOptions={studioControlOptions}
76
86
  embeddedBaseBundles={embeddedBaseBundles}
77
87
  />
@@ -96,6 +106,7 @@ type InnerProps = {
96
106
  captureTargetRef: React.RefObject<View | null>;
97
107
  style?: ViewStyle;
98
108
  showBubble: boolean;
109
+ enableAgentProgress: boolean;
99
110
  studioControlOptions?: import('@comergehq/studio-control').StudioControlOptions;
100
111
  embeddedBaseBundles?: EmbeddedBaseBundles;
101
112
  };
@@ -114,6 +125,7 @@ function ComergeStudioInner({
114
125
  captureTargetRef,
115
126
  style,
116
127
  showBubble,
128
+ enableAgentProgress,
117
129
  studioControlOptions,
118
130
  embeddedBaseBundles,
119
131
  }: InnerProps) {
@@ -179,6 +191,7 @@ function ComergeStudioInner({
179
191
  const threadId = app?.threadId ?? '';
180
192
  const thread = useThreadMessages(threadId);
181
193
  const editQueue = useEditQueue(activeAppId);
194
+ const agentProgress = useAgentRunProgress(threadId, { enabled: enableAgentProgress });
182
195
  const editQueueActions = useEditQueueActions(activeAppId);
183
196
  const [lastEditQueueInfo, setLastEditQueueInfo] = React.useState<{
184
197
  queueItemId?: string | null;
@@ -251,14 +264,20 @@ function ComergeStudioInner({
251
264
  const [upstreamSyncStatus, setUpstreamSyncStatus] = React.useState<SyncUpstreamStatus | null>(null);
252
265
  const isMrTestBuildInProgress = bundle.loading && bundle.loadingMode === 'test';
253
266
  const isBaseBundleDownloading = bundle.loading && bundle.loadingMode === 'base' && !bundle.isTesting;
267
+ const runtimePreparingText =
268
+ bundle.bundleStatus === 'pending'
269
+ ? 'Bundling app… this may take a few minutes'
270
+ : 'Preparing app…';
254
271
 
255
272
  // Show typing dots when the last message isn't an outcome (agent still working).
256
273
  const chatShowTypingIndicator = React.useMemo(() => {
274
+ if (agentProgress.hasLiveProgress) return false;
257
275
  if (!thread.raw || thread.raw.length === 0) return false;
258
276
  const last = thread.raw[thread.raw.length - 1];
259
277
  const payloadType = typeof (last.payload as any)?.type === 'string' ? String((last.payload as any).type) : undefined;
260
278
  return payloadType !== 'outcome';
261
- }, [thread.raw]);
279
+ }, [agentProgress.hasLiveProgress, thread.raw]);
280
+ const showChatProgress = agentProgress.hasLiveProgress || Boolean(agentProgress.view.bundle?.active);
262
281
 
263
282
  React.useEffect(() => {
264
283
  updateLastEditQueueInfo(null);
@@ -311,6 +330,7 @@ function ComergeStudioInner({
311
330
  <RuntimeRenderer
312
331
  appKey={appKey}
313
332
  bundlePath={bundle.bundlePath}
333
+ preparingText={runtimePreparingText}
314
334
  forcePreparing={showPostEditPreparing}
315
335
  renderToken={bundle.renderToken}
316
336
  allowInitialPreparing={!embeddedBaseBundles}
@@ -377,6 +397,7 @@ function ComergeStudioInner({
377
397
  onSendChat={(text, attachments) => actions.sendEdit({ prompt: text, attachments })}
378
398
  chatQueueItems={chatQueueItems}
379
399
  onRemoveQueueItem={(id) => editQueueActions.cancel(id)}
400
+ chatProgress={showChatProgress ? agentProgress.view : null}
380
401
  onNavigateHome={onNavigateHome}
381
402
  showBubble={showBubble}
382
403
  studioControlOptions={studioControlOptions}
@@ -0,0 +1,103 @@
1
+ import { Platform } from 'react-native';
2
+ import { Mixpanel } from 'mixpanel-react-native';
3
+
4
+ import { log } from '../../core/logger';
5
+ import type {
6
+ StudioAnalyticsEventMap,
7
+ StudioAnalyticsEventName,
8
+ StudioAnalyticsEventPayload,
9
+ } from './events';
10
+
11
+ type StudioAnalyticsInitOptions = {
12
+ enabled: boolean;
13
+ token?: string;
14
+ serverUrl?: string;
15
+ debug?: boolean;
16
+ };
17
+
18
+ let studioMixpanel: Mixpanel | null = null;
19
+ let studioAnalyticsEnabled = false;
20
+ let initPromise: Promise<void> | null = null;
21
+
22
+ export async function initStudioAnalytics(options: StudioAnalyticsInitOptions) {
23
+ if (initPromise) return initPromise;
24
+
25
+ initPromise = (async () => {
26
+ if (!options.enabled) {
27
+ studioAnalyticsEnabled = false;
28
+ return;
29
+ }
30
+
31
+ const token = (options.token ?? '').trim();
32
+ if (!token) {
33
+ studioAnalyticsEnabled = false;
34
+ log.warn('[studio-analytics] disabled: missing Mixpanel token');
35
+ return;
36
+ }
37
+
38
+ try {
39
+ const trackAutomaticEvents = false;
40
+ const useNative = false;
41
+ const serverUrl = (options.serverUrl ?? '').trim() || 'https://api.mixpanel.com';
42
+ const superProperties = {
43
+ runtime: 'comerge-studio',
44
+ platform: Platform.OS,
45
+ };
46
+
47
+ studioMixpanel = new Mixpanel(token, trackAutomaticEvents, useNative);
48
+ await studioMixpanel.init(false, superProperties, serverUrl);
49
+ studioMixpanel.setLoggingEnabled(Boolean(options.debug));
50
+ studioMixpanel.setFlushBatchSize(50);
51
+ studioAnalyticsEnabled = true;
52
+ } catch (error) {
53
+ studioMixpanel = null;
54
+ studioAnalyticsEnabled = false;
55
+ log.warn('[studio-analytics] init failed', error);
56
+ }
57
+ })();
58
+
59
+ return initPromise;
60
+ }
61
+
62
+ export function isStudioAnalyticsEnabled() {
63
+ return studioAnalyticsEnabled;
64
+ }
65
+
66
+ export async function trackStudioEvent<TName extends StudioAnalyticsEventName>(
67
+ eventName: TName,
68
+ properties: StudioAnalyticsEventPayload<TName>
69
+ ) {
70
+ if (!studioAnalyticsEnabled || !studioMixpanel) return;
71
+ try {
72
+ await studioMixpanel.track(eventName, properties as StudioAnalyticsEventMap[TName]);
73
+ } catch (error) {
74
+ log.warn('[studio-analytics] track failed', { eventName, error });
75
+ }
76
+ }
77
+
78
+ export async function flushStudioAnalytics() {
79
+ if (!studioAnalyticsEnabled || !studioMixpanel) return;
80
+ try {
81
+ await studioMixpanel.flush();
82
+ } catch (error) {
83
+ log.warn('[studio-analytics] flush failed', error);
84
+ }
85
+ }
86
+
87
+ export async function identifyStudioUser(userId: string) {
88
+ if (!studioAnalyticsEnabled || !studioMixpanel || !userId) return;
89
+ try {
90
+ await studioMixpanel.identify(userId);
91
+ } catch (error) {
92
+ log.warn('[studio-analytics] identify failed', error);
93
+ }
94
+ }
95
+
96
+ export async function resetStudioAnalytics() {
97
+ if (!studioAnalyticsEnabled || !studioMixpanel) return;
98
+ try {
99
+ await studioMixpanel.reset();
100
+ } catch (error) {
101
+ log.warn('[studio-analytics] reset failed', error);
102
+ }
103
+ }
@@ -0,0 +1,98 @@
1
+ export const STUDIO_ANALYTICS_EVENT_VERSION = 1 as const;
2
+
3
+ type ErrorMetadata = {
4
+ error_code?: string;
5
+ error_domain?: string;
6
+ };
7
+
8
+ export type InteractionSource = 'preview_panel' | 'unknown';
9
+
10
+ export type RemixAppEventProperties = {
11
+ app_id: string;
12
+ source_app_id: string;
13
+ thread_id?: string;
14
+ success: boolean;
15
+ } & ErrorMetadata;
16
+
17
+ export type EditAppEventProperties = {
18
+ app_id: string;
19
+ thread_id: string;
20
+ prompt_length: number;
21
+ success: boolean;
22
+ } & ErrorMetadata;
23
+
24
+ export type ShareAppEventProperties = {
25
+ app_id: string;
26
+ success: boolean;
27
+ } & ErrorMetadata;
28
+
29
+ export type OpenMergeRequestEventProperties = {
30
+ app_id: string;
31
+ merge_request_id?: string;
32
+ success: boolean;
33
+ } & ErrorMetadata;
34
+
35
+ export type ApproveMergeRequestEventProperties = {
36
+ app_id: string;
37
+ merge_request_id: string;
38
+ success: boolean;
39
+ } & ErrorMetadata;
40
+
41
+ export type RejectMergeRequestEventProperties = {
42
+ app_id: string;
43
+ merge_request_id: string;
44
+ success: boolean;
45
+ } & ErrorMetadata;
46
+
47
+ export type TestBundleEventProperties = {
48
+ app_id: string;
49
+ commit_id?: string;
50
+ success: boolean;
51
+ } & ErrorMetadata;
52
+
53
+ export type LikeAppEventProperties = {
54
+ app_id: string;
55
+ source: InteractionSource;
56
+ success: boolean;
57
+ } & ErrorMetadata;
58
+
59
+ export type UnlikeAppEventProperties = {
60
+ app_id: string;
61
+ source: InteractionSource;
62
+ success: boolean;
63
+ } & ErrorMetadata;
64
+
65
+ export type OpenCommentsEventProperties = {
66
+ app_id: string;
67
+ source: InteractionSource;
68
+ };
69
+
70
+ export type SubmitCommentEventProperties = {
71
+ app_id: string;
72
+ comment_type: 'general';
73
+ comment_length: number;
74
+ success: boolean;
75
+ } & ErrorMetadata;
76
+
77
+ export type StudioAnalyticsEventMap = {
78
+ remix_app: RemixAppEventProperties;
79
+ edit_app: EditAppEventProperties;
80
+ share_app: ShareAppEventProperties;
81
+ open_merge_request: OpenMergeRequestEventProperties;
82
+ approve_merge_request: ApproveMergeRequestEventProperties;
83
+ reject_merge_request: RejectMergeRequestEventProperties;
84
+ test_bundle: TestBundleEventProperties;
85
+ like_app: LikeAppEventProperties;
86
+ unlike_app: UnlikeAppEventProperties;
87
+ open_comments: OpenCommentsEventProperties;
88
+ submit_comment: SubmitCommentEventProperties;
89
+ };
90
+
91
+ export type StudioAnalyticsEventName = keyof StudioAnalyticsEventMap;
92
+
93
+ export type StudioAnalyticsBaseProperties = {
94
+ event_version: typeof STUDIO_ANALYTICS_EVENT_VERSION;
95
+ };
96
+
97
+ export type StudioAnalyticsEventPayload<TName extends StudioAnalyticsEventName> =
98
+ StudioAnalyticsEventMap[TName] & StudioAnalyticsBaseProperties;
@@ -0,0 +1,237 @@
1
+ import { flushStudioAnalytics, trackStudioEvent } from './client';
2
+ import {
3
+ STUDIO_ANALYTICS_EVENT_VERSION,
4
+ type InteractionSource,
5
+ type StudioAnalyticsEventPayload,
6
+ } from './events';
7
+
8
+ function baseProps() {
9
+ return { event_version: STUDIO_ANALYTICS_EVENT_VERSION } as const;
10
+ }
11
+
12
+ function normalizeError(error: unknown): { error_code?: string; error_domain?: string } {
13
+ if (!error) return {};
14
+
15
+ if (typeof error === 'string') {
16
+ return { error_code: error.slice(0, 120), error_domain: 'string' };
17
+ }
18
+
19
+ if (error instanceof Error) {
20
+ return {
21
+ error_code: error.message.slice(0, 120),
22
+ error_domain: error.name || 'Error',
23
+ };
24
+ }
25
+
26
+ if (typeof error === 'object') {
27
+ const candidate = error as { code?: string | number; name?: string; message?: string };
28
+ return {
29
+ error_code: String(candidate.code ?? candidate.message ?? 'unknown_error').slice(0, 120),
30
+ error_domain: candidate.name ?? 'object',
31
+ };
32
+ }
33
+
34
+ return { error_code: 'unknown_error', error_domain: typeof error };
35
+ }
36
+
37
+ async function trackMutationEvent<TName extends keyof Omit<{
38
+ remix_app: true;
39
+ edit_app: true;
40
+ share_app: true;
41
+ open_merge_request: true;
42
+ approve_merge_request: true;
43
+ reject_merge_request: true;
44
+ test_bundle: true;
45
+ like_app: true;
46
+ unlike_app: true;
47
+ submit_comment: true;
48
+ }, never>>(
49
+ name: TName,
50
+ payload: StudioAnalyticsEventPayload<TName>
51
+ ) {
52
+ await trackStudioEvent(name, payload);
53
+ await flushStudioAnalytics();
54
+ }
55
+
56
+ let lastOpenCommentsKey: string | null = null;
57
+ let lastOpenCommentsAt = 0;
58
+
59
+ export async function trackRemixApp(params: {
60
+ appId: string;
61
+ sourceAppId: string;
62
+ threadId?: string;
63
+ success: boolean;
64
+ error?: unknown;
65
+ }) {
66
+ const errorProps = params.success ? {} : normalizeError(params.error);
67
+ await trackMutationEvent('remix_app', {
68
+ app_id: params.appId,
69
+ source_app_id: params.sourceAppId,
70
+ thread_id: params.threadId,
71
+ success: params.success,
72
+ ...errorProps,
73
+ ...baseProps(),
74
+ });
75
+ }
76
+
77
+ export async function trackEditApp(params: {
78
+ appId: string;
79
+ threadId: string;
80
+ promptLength: number;
81
+ success: boolean;
82
+ error?: unknown;
83
+ }) {
84
+ const errorProps = params.success ? {} : normalizeError(params.error);
85
+ await trackMutationEvent('edit_app', {
86
+ app_id: params.appId,
87
+ thread_id: params.threadId,
88
+ prompt_length: params.promptLength,
89
+ success: params.success,
90
+ ...errorProps,
91
+ ...baseProps(),
92
+ });
93
+ }
94
+
95
+ export async function trackShareApp(params: {
96
+ appId: string;
97
+ success: boolean;
98
+ error?: unknown;
99
+ }) {
100
+ const errorProps = params.success ? {} : normalizeError(params.error);
101
+ await trackMutationEvent('share_app', {
102
+ app_id: params.appId,
103
+ success: params.success,
104
+ ...errorProps,
105
+ ...baseProps(),
106
+ });
107
+ }
108
+
109
+ export async function trackOpenMergeRequest(params: {
110
+ appId: string;
111
+ mergeRequestId?: string;
112
+ success: boolean;
113
+ error?: unknown;
114
+ }) {
115
+ const errorProps = params.success ? {} : normalizeError(params.error);
116
+ await trackMutationEvent('open_merge_request', {
117
+ app_id: params.appId,
118
+ merge_request_id: params.mergeRequestId,
119
+ success: params.success,
120
+ ...errorProps,
121
+ ...baseProps(),
122
+ });
123
+ }
124
+
125
+ export async function trackApproveMergeRequest(params: {
126
+ appId: string;
127
+ mergeRequestId: string;
128
+ success: boolean;
129
+ error?: unknown;
130
+ }) {
131
+ const errorProps = params.success ? {} : normalizeError(params.error);
132
+ await trackMutationEvent('approve_merge_request', {
133
+ app_id: params.appId,
134
+ merge_request_id: params.mergeRequestId,
135
+ success: params.success,
136
+ ...errorProps,
137
+ ...baseProps(),
138
+ });
139
+ }
140
+
141
+ export async function trackRejectMergeRequest(params: {
142
+ appId: string;
143
+ mergeRequestId: string;
144
+ success: boolean;
145
+ error?: unknown;
146
+ }) {
147
+ const errorProps = params.success ? {} : normalizeError(params.error);
148
+ await trackMutationEvent('reject_merge_request', {
149
+ app_id: params.appId,
150
+ merge_request_id: params.mergeRequestId,
151
+ success: params.success,
152
+ ...errorProps,
153
+ ...baseProps(),
154
+ });
155
+ }
156
+
157
+ export async function trackTestBundle(params: {
158
+ appId: string;
159
+ commitId?: string;
160
+ success: boolean;
161
+ error?: unknown;
162
+ }) {
163
+ const errorProps = params.success ? {} : normalizeError(params.error);
164
+ await trackMutationEvent('test_bundle', {
165
+ app_id: params.appId,
166
+ commit_id: params.commitId,
167
+ success: params.success,
168
+ ...errorProps,
169
+ ...baseProps(),
170
+ });
171
+ }
172
+
173
+ export async function trackLikeApp(params: {
174
+ appId: string;
175
+ source?: InteractionSource;
176
+ success: boolean;
177
+ error?: unknown;
178
+ }) {
179
+ const errorProps = params.success ? {} : normalizeError(params.error);
180
+ await trackMutationEvent('like_app', {
181
+ app_id: params.appId,
182
+ source: params.source ?? 'unknown',
183
+ success: params.success,
184
+ ...errorProps,
185
+ ...baseProps(),
186
+ });
187
+ }
188
+
189
+ export async function trackUnlikeApp(params: {
190
+ appId: string;
191
+ source?: InteractionSource;
192
+ success: boolean;
193
+ error?: unknown;
194
+ }) {
195
+ const errorProps = params.success ? {} : normalizeError(params.error);
196
+ await trackMutationEvent('unlike_app', {
197
+ app_id: params.appId,
198
+ source: params.source ?? 'unknown',
199
+ success: params.success,
200
+ ...errorProps,
201
+ ...baseProps(),
202
+ });
203
+ }
204
+
205
+ export async function trackOpenComments(params: {
206
+ appId: string;
207
+ source?: InteractionSource;
208
+ }) {
209
+ const key = `${params.appId}:${params.source ?? 'unknown'}`;
210
+ const now = Date.now();
211
+ if (lastOpenCommentsKey === key && now - lastOpenCommentsAt < 1000) return;
212
+ lastOpenCommentsKey = key;
213
+ lastOpenCommentsAt = now;
214
+
215
+ await trackStudioEvent('open_comments', {
216
+ app_id: params.appId,
217
+ source: params.source ?? 'unknown',
218
+ ...baseProps(),
219
+ });
220
+ }
221
+
222
+ export async function trackSubmitComment(params: {
223
+ appId: string;
224
+ commentLength: number;
225
+ success: boolean;
226
+ error?: unknown;
227
+ }) {
228
+ const errorProps = params.success ? {} : normalizeError(params.error);
229
+ await trackMutationEvent('submit_comment', {
230
+ app_id: params.appId,
231
+ comment_type: 'general',
232
+ comment_length: params.commentLength,
233
+ success: params.success,
234
+ ...errorProps,
235
+ ...baseProps(),
236
+ });
237
+ }
@@ -16,8 +16,14 @@ export type StudioBootstrapProps = UseStudioBootstrapOptions & {
16
16
  renderError?: (error: Error) => React.ReactNode;
17
17
  };
18
18
 
19
- export function StudioBootstrap({ children, fallback, renderError, clientKey }: StudioBootstrapProps) {
20
- const { ready, error, userId } = useStudioBootstrap({ clientKey });
19
+ export function StudioBootstrap({
20
+ children,
21
+ fallback,
22
+ renderError,
23
+ clientKey,
24
+ analyticsEnabled,
25
+ }: StudioBootstrapProps) {
26
+ const { ready, error, userId } = useStudioBootstrap({ clientKey, analyticsEnabled });
21
27
 
22
28
  if (error) {
23
29
  return (
@@ -3,11 +3,13 @@ import * as React from 'react';
3
3
  import { setClientKey } from '../../core/services/http/public';
4
4
  import { ensureAuthenticatedSession, ensureAnonymousSession } from '../../core/services/supabase/auth';
5
5
  import { isSupabaseClientInjected, setSupabaseConfig } from '../../core/services/supabase/client';
6
+ import { identifyStudioUser, initStudioAnalytics, resetStudioAnalytics } from '../analytics/client';
6
7
  const SUPABASE_URL = 'https://xtfxwbckjpfmqubnsusu.supabase.co';
7
8
  const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0Znh3YmNranBmbXF1Ym5zdXN1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA2MDEyMzAsImV4cCI6MjA3NjE3NzIzMH0.dzWGAWrK4CvrmHVHzf8w7JlUZohdap0ZPnLZnABMV8s';
8
9
 
9
10
  export type UseStudioBootstrapOptions = {
10
11
  clientKey: string;
12
+ analyticsEnabled?: boolean;
11
13
  };
12
14
 
13
15
  export type StudioBootstrapState = {
@@ -29,11 +31,25 @@ export function useStudioBootstrap(options: UseStudioBootstrapOptions): StudioBo
29
31
  (async () => {
30
32
  try {
31
33
  setClientKey(options.clientKey);
32
- const requireAuth = isSupabaseClientInjected();
34
+ const hasInjectedSupabase = isSupabaseClientInjected();
35
+ const requireAuth = hasInjectedSupabase;
36
+ const analyticsEnabled = options.analyticsEnabled ?? hasInjectedSupabase;
37
+
38
+ await initStudioAnalytics({
39
+ enabled: analyticsEnabled,
40
+ token: process.env.EXPO_PUBLIC_MIXPANEL_TOKEN,
41
+ serverUrl: process.env.EXPO_PUBLIC_MIXPANEL_SERVER_URL,
42
+ debug: __DEV__,
43
+ });
44
+
33
45
  if (!requireAuth) {
34
46
  setSupabaseConfig({ url: SUPABASE_URL, anonKey: SUPABASE_ANON_KEY });
47
+ await resetStudioAnalytics();
35
48
  }
36
49
  const { user } = requireAuth ? await ensureAuthenticatedSession() : await ensureAnonymousSession();
50
+ if (requireAuth) {
51
+ await identifyStudioUser(user.id);
52
+ }
37
53
 
38
54
  if (cancelled) return;
39
55
  setState({ ready: true, userId: user.id, error: null });
@@ -47,7 +63,7 @@ export function useStudioBootstrap(options: UseStudioBootstrapOptions): StudioBo
47
63
  return () => {
48
64
  cancelled = true;
49
65
  };
50
- }, [options.clientKey]);
66
+ }, [options.analyticsEnabled, options.clientKey]);
51
67
 
52
68
  return state;
53
69
  }