@@ -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,11 +220,11 @@ export function useDowngradeToPersonal() {
207220 )
208221 }
209222 ensureCanDowngrade ( )
210- targetTier =
223+ telemetryAttempt . targetTier =
211224 preview . new_plan ?. tier && preview . new_plan . tier !== 'TEAM'
212225 ? TIER_TO_KEY [ preview . new_plan . tier ]
213226 : undefined
214- targetCycle = preview . new_plan
227+ telemetryAttempt . targetCycle = preview . new_plan
215228 ? preview . new_plan . duration === 'ANNUAL'
216229 ? 'yearly'
217230 : 'monthly'
@@ -245,7 +258,7 @@ export function useDowngradeToPersonal() {
245258 try {
246259 await workspaceStore . removeMember ( member . id )
247260 } catch ( error ) {
248- memberRemovalFailures += 1
261+ telemetryAttempt . memberRemovalFailures += 1
249262 telemetryFailure = {
250263 failure_category : categorizeBillingApiError ( error ) ,
251264 error_code : 'member_removal_failed'
@@ -260,24 +273,26 @@ export function useDowngradeToPersonal() {
260273 }
261274
262275 ensureCanDowngrade ( )
263- checkoutStartedAt = Date . now ( )
264- telemetry ?. trackBillingEvent ( {
265- operation : 'subscription_checkout' ,
266- stage : 'started' ,
267- outcome : 'pending' ,
268- tier : targetTier ,
269- cycle : targetCycle ,
270- checkout_type : 'change'
271- } )
272- telemetry ?. trackBillingEvent ( {
273- operation : 'operation' ,
274- stage : 'started' ,
275- outcome : 'pending' ,
276- operation_type : 'subscription' ,
277- tier : targetTier ,
278- cycle : targetCycle ,
279- checkout_type : 'change'
280- } )
276+ if ( telemetryAttempt . checkoutStartedAt === undefined ) {
277+ telemetryAttempt . checkoutStartedAt = Date . now ( )
278+ telemetry ?. trackBillingEvent ( {
279+ operation : 'subscription_checkout' ,
280+ stage : 'started' ,
281+ outcome : 'pending' ,
282+ tier : telemetryAttempt . targetTier ,
283+ cycle : telemetryAttempt . targetCycle ,
284+ checkout_type : 'change'
285+ } )
286+ telemetry ?. trackBillingEvent ( {
287+ operation : 'operation' ,
288+ stage : 'started' ,
289+ outcome : 'pending' ,
290+ operation_type : 'subscription' ,
291+ tier : telemetryAttempt . targetTier ,
292+ cycle : telemetryAttempt . targetCycle ,
293+ checkout_type : 'change'
294+ } )
295+ }
281296 let response : SubscribeResponse | void
282297 try {
283298 response = await subscribe ( planSlug , {
@@ -331,18 +346,19 @@ export function useDowngradeToPersonal() {
331346 response . billing_op_id ,
332347 'subscription' ,
333348 {
334- tier : targetTier ,
335- cycle : targetCycle ,
349+ tier : telemetryAttempt . targetTier ,
350+ cycle : telemetryAttempt . targetCycle ,
336351 checkoutType : 'change' ,
337352 downgradeToPersonal : {
338- memberRemovalCount : membersToRemove . length ,
339- memberRemovalFailures,
340- targetTier,
341- startedAt : downgradeStartedAt
353+ memberRemovalCount : telemetryAttempt . memberRemovalCount ,
354+ memberRemovalFailures : telemetryAttempt . memberRemovalFailures ,
355+ targetTier : telemetryAttempt . targetTier ,
356+ startedAt : telemetryAttempt . startedAt
342357 } ,
343- attemptStartedAt : checkoutStartedAt
358+ attemptStartedAt : telemetryAttempt . checkoutStartedAt
344359 }
345360 )
361+ activeTelemetryAttempt = undefined
346362 return null
347363 }
348364
@@ -351,24 +367,29 @@ export function useDowngradeToPersonal() {
351367 response . billing_op_id ,
352368 'subscription' ,
353369 {
354- tier : targetTier ,
355- cycle : targetCycle ,
370+ tier : telemetryAttempt . targetTier ,
371+ cycle : telemetryAttempt . targetCycle ,
356372 checkoutType : 'change' ,
357373 downgradeToPersonal : {
358- memberRemovalCount : membersToRemove . length ,
359- memberRemovalFailures,
360- targetTier,
361- startedAt : downgradeStartedAt
374+ memberRemovalCount : telemetryAttempt . memberRemovalCount ,
375+ memberRemovalFailures : telemetryAttempt . memberRemovalFailures ,
376+ targetTier : telemetryAttempt . targetTier ,
377+ startedAt : telemetryAttempt . startedAt
362378 } ,
363- attemptStartedAt : checkoutStartedAt
379+ attemptStartedAt : telemetryAttempt . checkoutStartedAt
364380 }
365381 )
382+ activeTelemetryAttempt = undefined
366383 return null
367384 }
368385
369386 trackSucceeded ( )
387+ activeTelemetryAttempt = undefined
370388 return { preview, response }
371389 } catch ( error ) {
390+ if ( error instanceof ReactivationConfirmationRequiredError ) {
391+ throw error
392+ }
372393 const failure = telemetryFailure ?? {
373394 failure_category : categorizeBillingApiError ( error )
374395 }
@@ -377,35 +398,36 @@ export function useDowngradeToPersonal() {
377398 operation : 'downgrade_to_personal' ,
378399 stage : 'failed' ,
379400 outcome : 'failure' ,
380- member_removal_count : membersToRemove . length ,
381- member_removal_failures : memberRemovalFailures ,
382- target_tier : targetTier ,
401+ member_removal_count : telemetryAttempt . memberRemovalCount ,
402+ member_removal_failures : telemetryAttempt . memberRemovalFailures ,
403+ target_tier : telemetryAttempt . targetTier ,
383404 ...failure ,
384- duration_ms : now - downgradeStartedAt
405+ duration_ms : now - telemetryAttempt . startedAt
385406 } )
386- if ( checkoutStartedAt !== undefined ) {
407+ if ( telemetryAttempt . checkoutStartedAt !== undefined ) {
387408 telemetry ?. trackBillingEvent ( {
388409 operation : 'subscription_checkout' ,
389410 stage : 'failed' ,
390411 outcome : 'failure' ,
391- tier : targetTier ,
392- cycle : targetCycle ,
412+ tier : telemetryAttempt . targetTier ,
413+ cycle : telemetryAttempt . targetCycle ,
393414 checkout_type : 'change' ,
394415 ...failure ,
395- duration_ms : now - checkoutStartedAt
416+ duration_ms : now - telemetryAttempt . checkoutStartedAt
396417 } )
397418 telemetry ?. trackBillingEvent ( {
398419 operation : 'operation' ,
399420 stage : 'failed' ,
400421 outcome : 'failure' ,
401422 operation_type : 'subscription' ,
402- tier : targetTier ,
403- cycle : targetCycle ,
423+ tier : telemetryAttempt . targetTier ,
424+ cycle : telemetryAttempt . targetCycle ,
404425 checkout_type : 'change' ,
405426 ...failure ,
406- duration_ms : now - checkoutStartedAt
427+ duration_ms : now - telemetryAttempt . checkoutStartedAt
407428 } )
408429 }
430+ activeTelemetryAttempt = undefined
409431 throw error
410432 }
411433 }
0 commit comments