@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.
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1576 -601
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1292 -317
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/components/chat/AgentProgressCard.tsx +82 -0
- package/src/components/chat/BundleProgressCard.tsx +75 -0
- package/src/components/chat/ChatMessageBubble.tsx +48 -4
- package/src/components/chat/ChatMessageList.tsx +56 -36
- package/src/components/comments/useAppComments.ts +12 -0
- package/src/data/agent-progress/repository.ts +179 -0
- package/src/data/agent-progress/types.ts +67 -0
- package/src/studio/ComergeStudio.tsx +23 -2
- package/src/studio/analytics/client.ts +103 -0
- package/src/studio/analytics/events.ts +98 -0
- package/src/studio/analytics/track.ts +237 -0
- package/src/studio/bootstrap/StudioBootstrap.tsx +8 -2
- package/src/studio/bootstrap/useStudioBootstrap.ts +18 -2
- package/src/studio/hooks/useAgentRunProgress.ts +357 -0
- package/src/studio/hooks/useAppStats.ts +14 -2
- package/src/studio/hooks/useBundleManager.ts +43 -5
- package/src/studio/hooks/useMergeRequests.ts +63 -14
- package/src/studio/hooks/useStudioActions.ts +34 -1
- package/src/studio/ui/ChatPanel.tsx +13 -2
- package/src/studio/ui/PreviewPanel.tsx +10 -0
- package/src/studio/ui/RuntimeRenderer.tsx +6 -1
- package/src/studio/ui/StudioOverlay.tsx +3 -0
- package/src/studio/ui/preview-panel/usePreviewPanelData.ts +1 -0
|
@@ -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
|
|
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({
|
|
20
|
-
|
|
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
|
|
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
|
}
|