@@ -31,6 +31,15 @@ export interface DowngradePreview {
3131 requiresReactivationConfirmation : boolean
3232}
3333
34+ interface DowngradeTelemetryAttempt {
35+ startedAt : number
36+ memberRemovalCount : number
37+ memberRemovalFailures : number
38+ targetTier ?: TierKey
39+ targetCycle ?: BillingCycle
40+ checkoutStartedAt ?: number
41+ }
42+
3443/** Thrown by `downgradeToPersonal` when the billing authority requires
3544 * reactivation consent, so the still-open confirmation can collect it and
3645 * retry with `confirmReactivation: true`. */
@@ -66,6 +75,7 @@ export function useDowngradeToPersonal() {
6675 const { userEmail } = useCurrentUser ( )
6776 const { permissions } = useWorkspaceUI ( )
6877 const telemetry = useTelemetry ( )
78+ let activeTelemetryAttempt : DowngradeTelemetryAttempt | undefined
6979
7080 const removableMembers = computed ( ( ) => {
7181 const hasFlag = members . value . some ( ( m ) => m . isOriginalOwner )
@@ -147,51 +157,54 @@ export function useDowngradeToPersonal() {
147157 ensureCanDowngrade ( )
148158
149159 const membersToRemove = removableMembers . value
150- let memberRemovalFailures = 0
151- let targetTier : TierKey | undefined
152- let targetCycle : BillingCycle | undefined
160+ const telemetryAttempt = activeTelemetryAttempt ?? {
161+ startedAt : Date . now ( ) ,
162+ memberRemovalCount : membersToRemove . length ,
163+ memberRemovalFailures : 0
164+ }
153165 let telemetryFailure : BillingFailure | undefined
154- let checkoutStartedAt : number | undefined
155166
156- const downgradeStartedAt = Date . now ( )
157- telemetry ?. trackBillingEvent ( {
158- operation : 'downgrade_to_personal' ,
159- stage : 'started' ,
160- outcome : 'pending' ,
161- member_removal_count : membersToRemove . length ,
162- member_removal_failures : 0
163- } )
167+ if ( ! activeTelemetryAttempt ) {
168+ activeTelemetryAttempt = telemetryAttempt
169+ telemetry ?. trackBillingEvent ( {
170+ operation : 'downgrade_to_personal' ,
171+ stage : 'started' ,
172+ outcome : 'pending' ,
173+ member_removal_count : telemetryAttempt . memberRemovalCount ,
174+ member_removal_failures : 0
175+ } )
176+ }
164177
165178 function trackSucceeded ( ) {
166179 const now = Date . now ( )
167180 telemetry ?. trackBillingEvent ( {
168181 operation : 'downgrade_to_personal' ,
169182 stage : 'succeeded' ,
170183 outcome : 'success' ,
171- member_removal_count : membersToRemove . length ,
172- member_removal_failures : memberRemovalFailures ,
173- target_tier : targetTier ,
174- duration_ms : now - downgradeStartedAt
184+ member_removal_count : telemetryAttempt . memberRemovalCount ,
185+ member_removal_failures : telemetryAttempt . memberRemovalFailures ,
186+ target_tier : telemetryAttempt . targetTier ,
187+ duration_ms : now - telemetryAttempt . startedAt
175188 } )
176- if ( checkoutStartedAt === undefined ) return
189+ if ( telemetryAttempt . checkoutStartedAt === undefined ) return
177190 telemetry ?. trackBillingEvent ( {
178191 operation : 'subscription_checkout' ,
179192 stage : 'succeeded' ,
180193 outcome : 'success' ,
181- tier : targetTier ,
182- cycle : targetCycle ,
194+ tier : telemetryAttempt . targetTier ,
195+ cycle : telemetryAttempt . targetCycle ,
183196 checkout_type : 'change' ,
184- duration_ms : now - checkoutStartedAt
197+ duration_ms : now - telemetryAttempt . checkoutStartedAt
185198 } )
186199 telemetry ?. trackBillingEvent ( {
187200 operation : 'operation' ,
188201 stage : 'succeeded' ,
189202 outcome : 'success' ,
190203 operation_type : 'subscription' ,
191- tier : targetTier ,
192- cycle : targetCycle ,
204+ tier : telemetryAttempt . targetTier ,
205+ cycle : telemetryAttempt . targetCycle ,
193206 checkout_type : 'change' ,
194- duration_ms : now - checkoutStartedAt
207+ duration_ms : now - telemetryAttempt . checkoutStartedAt
195208 } )
196209 }
197210
@@ -207,10 +220,10 @@ export function useDowngradeToPersonal() {
207220 )
208221 }
209222 ensureCanDowngrade ( )
210- targetTier = preview . new_plan ?. tier
223+ telemetryAttempt . targetTier = preview . new_plan ?. tier
211224 ? ( toTierKey ( preview . new_plan . tier ) ?? undefined )
212225 : undefined
213- targetCycle = preview . new_plan
226+ telemetryAttempt . targetCycle = preview . new_plan
214227 ? preview . new_plan . duration === 'ANNUAL'
215228 ? 'yearly'
216229 : 'monthly'
@@ -244,7 +257,7 @@ export function useDowngradeToPersonal() {
244257 try {
245258 await workspaceStore . removeMember ( member . id )
246259 } catch ( error ) {
247- memberRemovalFailures += 1
260+ telemetryAttempt . memberRemovalFailures += 1
248261 telemetryFailure = {
249262 failure_category : categorizeBillingApiError ( error ) ,
250263 error_code : 'member_removal_failed'
@@ -259,24 +272,26 @@ export function useDowngradeToPersonal() {
259272 }
260273
261274 ensureCanDowngrade ( )
262- checkoutStartedAt = Date . now ( )
263- telemetry ?. trackBillingEvent ( {
264- operation : 'subscription_checkout' ,
265- stage : 'started' ,
266- outcome : 'pending' ,
267- tier : targetTier ,
268- cycle : targetCycle ,
269- checkout_type : 'change'
270- } )
271- telemetry ?. trackBillingEvent ( {
272- operation : 'operation' ,
273- stage : 'started' ,
274- outcome : 'pending' ,
275- operation_type : 'subscription' ,
276- tier : targetTier ,
277- cycle : targetCycle ,
278- checkout_type : 'change'
279- } )
275+ if ( telemetryAttempt . checkoutStartedAt === undefined ) {
276+ telemetryAttempt . checkoutStartedAt = Date . now ( )
277+ telemetry ?. trackBillingEvent ( {
278+ operation : 'subscription_checkout' ,
279+ stage : 'started' ,
280+ outcome : 'pending' ,
281+ tier : telemetryAttempt . targetTier ,
282+ cycle : telemetryAttempt . targetCycle ,
283+ checkout_type : 'change'
284+ } )
285+ telemetry ?. trackBillingEvent ( {
286+ operation : 'operation' ,
287+ stage : 'started' ,
288+ outcome : 'pending' ,
289+ operation_type : 'subscription' ,
290+ tier : telemetryAttempt . targetTier ,
291+ cycle : telemetryAttempt . targetCycle ,
292+ checkout_type : 'change'
293+ } )
294+ }
280295 let response : SubscribeResponse | void
281296 try {
282297 response = await subscribe ( planSlug , {
@@ -330,18 +345,19 @@ export function useDowngradeToPersonal() {
330345 response . billing_op_id ,
331346 'subscription' ,
332347 {
333- tier : targetTier ,
334- cycle : targetCycle ,
348+ tier : telemetryAttempt . targetTier ,
349+ cycle : telemetryAttempt . targetCycle ,
335350 checkoutType : 'change' ,
336351 downgradeToPersonal : {
337- memberRemovalCount : membersToRemove . length ,
338- memberRemovalFailures,
339- targetTier,
340- startedAt : downgradeStartedAt
352+ memberRemovalCount : telemetryAttempt . memberRemovalCount ,
353+ memberRemovalFailures : telemetryAttempt . memberRemovalFailures ,
354+ targetTier : telemetryAttempt . targetTier ,
355+ startedAt : telemetryAttempt . startedAt
341356 } ,
342- attemptStartedAt : checkoutStartedAt
357+ attemptStartedAt : telemetryAttempt . checkoutStartedAt
343358 }
344359 )
360+ activeTelemetryAttempt = undefined
345361 return null
346362 }
347363
@@ -350,24 +366,29 @@ export function useDowngradeToPersonal() {
350366 response . billing_op_id ,
351367 'subscription' ,
352368 {
353- tier : targetTier ,
354- cycle : targetCycle ,
369+ tier : telemetryAttempt . targetTier ,
370+ cycle : telemetryAttempt . targetCycle ,
355371 checkoutType : 'change' ,
356372 downgradeToPersonal : {
357- memberRemovalCount : membersToRemove . length ,
358- memberRemovalFailures,
359- targetTier,
360- startedAt : downgradeStartedAt
373+ memberRemovalCount : telemetryAttempt . memberRemovalCount ,
374+ memberRemovalFailures : telemetryAttempt . memberRemovalFailures ,
375+ targetTier : telemetryAttempt . targetTier ,
376+ startedAt : telemetryAttempt . startedAt
361377 } ,
362- attemptStartedAt : checkoutStartedAt
378+ attemptStartedAt : telemetryAttempt . checkoutStartedAt
363379 }
364380 )
381+ activeTelemetryAttempt = undefined
365382 return null
366383 }
367384
368385 trackSucceeded ( )
386+ activeTelemetryAttempt = undefined
369387 return { preview, response }
370388 } catch ( error ) {
389+ if ( error instanceof ReactivationConfirmationRequiredError ) {
390+ throw error
391+ }
371392 const failure = telemetryFailure ?? {
372393 failure_category : categorizeBillingApiError ( error )
373394 }
@@ -376,35 +397,36 @@ export function useDowngradeToPersonal() {
376397 operation : 'downgrade_to_personal' ,
377398 stage : 'failed' ,
378399 outcome : 'failure' ,
379- member_removal_count : membersToRemove . length ,
380- member_removal_failures : memberRemovalFailures ,
381- target_tier : targetTier ,
400+ member_removal_count : telemetryAttempt . memberRemovalCount ,
401+ member_removal_failures : telemetryAttempt . memberRemovalFailures ,
402+ target_tier : telemetryAttempt . targetTier ,
382403 ...failure ,
383- duration_ms : now - downgradeStartedAt
404+ duration_ms : now - telemetryAttempt . startedAt
384405 } )
385- if ( checkoutStartedAt !== undefined ) {
406+ if ( telemetryAttempt . checkoutStartedAt !== undefined ) {
386407 telemetry ?. trackBillingEvent ( {
387408 operation : 'subscription_checkout' ,
388409 stage : 'failed' ,
389410 outcome : 'failure' ,
390- tier : targetTier ,
391- cycle : targetCycle ,
411+ tier : telemetryAttempt . targetTier ,
412+ cycle : telemetryAttempt . targetCycle ,
392413 checkout_type : 'change' ,
393414 ...failure ,
394- duration_ms : now - checkoutStartedAt
415+ duration_ms : now - telemetryAttempt . checkoutStartedAt
395416 } )
396417 telemetry ?. trackBillingEvent ( {
397418 operation : 'operation' ,
398419 stage : 'failed' ,
399420 outcome : 'failure' ,
400421 operation_type : 'subscription' ,
401- tier : targetTier ,
402- cycle : targetCycle ,
422+ tier : telemetryAttempt . targetTier ,
423+ cycle : telemetryAttempt . targetCycle ,
403424 checkout_type : 'change' ,
404425 ...failure ,
405- duration_ms : now - checkoutStartedAt
426+ duration_ms : now - telemetryAttempt . checkoutStartedAt
406427 } )
407428 }
429+ activeTelemetryAttempt = undefined
408430 throw error
409431 }
410432 }
0 commit comments