Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cli/src/app/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ function describeFetchFailure(error: unknown, endpoint: string) {

export type { AppDebugOptions as OptionsBaseDebug } from '../schemas/app'

export async function markSnag(channel: string, orgId: string, apikey: string, event: string, appId?: string, icon = '✅') {
export async function markSnag(channel: string, orgId: string, apikey: string, event: string, appId?: string, icon = '✅', tags: Record<string, string | number | boolean> = {}) {
const allTags = { ...(appId ? { 'app-id': appId } : {}), ...tags }
await sendEvent(apikey, {
channel,
event,
icon,
org_id: orgId,
tracking_version: 2,
...(appId ? { tags: { 'app-id': appId } } : {}),
...(Object.keys(allTags).length > 0 ? { tags: allTags } : {}),
notify: false,
})
}
Expand Down
7 changes: 5 additions & 2 deletions cli/src/app/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { version as nodeVersion } from 'node:process'
import { log, spinner } from '@clack/prompts'
import pack from '../../package.json'
import { trackEvent } from '../analytics/track'
import { getAllPackagesDependencies, getAppId, getBundleVersion, getConfig } from '../utils'
import { getAllPackagesDependencies, getAppId, getBundleVersion, getCapgoPluginTags, getConfig } from '../utils'
import { getLatestVersion } from '../utils/latest-version'

async function getLatestDependencies(installedDependencies: Record<string, string>) {
Expand Down Expand Up @@ -109,7 +109,10 @@ export async function getInfoInternal(options: DoctorInfoOptions, silent = false
channel: 'cli-usage',
event: 'Doctor Ran',
icon: '👨‍⚕️',
tags: computeDoctorAnalyticsTags(installedDependencies, latestDependencies),
tags: {
...computeDoctorAnalyticsTags(installedDependencies, latestDependencies),
...getCapgoPluginTags(options.packageJson),
},
})

if (JSON.stringify(installedDependencies) !== JSON.stringify(latestDependencies)) {
Expand Down
4 changes: 3 additions & 1 deletion cli/src/bundle/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { confirmWithRememberedChoice } from '../promptPreferences'
import { showReplicationProgress } from '../replicationProgress'
import { formatTable } from '../terminal-table'
import { usesAlwaysDirectUpdate } from '../updaterConfig'
import { baseKeyV2, BROTLI_MIN_UPDATER_VERSION_V5, BROTLI_MIN_UPDATER_VERSION_V6, BROTLI_MIN_UPDATER_VERSION_V7, canPromptInteractively, checkCompatibilityCloud, checkPlanValidUpload, checkRemoteCliMessages, createSupabaseClient, deletedFailedVersion, findRoot, findSavedKey, formatError, getAppId, getBundleVersion, getCompatibilityDetails, getConfig, getInstalledVersion, getLocalConfig, getLocalDependencies, getOrganizationId, getPMAndCommand, getRemoteChecksums, getRemoteFileConfig, hasCliPermission, hasOrganizationPerm, isCompatible, isDeprecatedPluginVersion, OrganizationPerm, regexSemver, resolveUserIdFromApiKey, sendEvent, updateConfigUpdater, updateOrCreateChannel, updateOrCreateVersion, UPLOAD_TIMEOUT, uploadTUS, uploadUrl, zipFile } from '../utils'
import { baseKeyV2, BROTLI_MIN_UPDATER_VERSION_V5, BROTLI_MIN_UPDATER_VERSION_V6, BROTLI_MIN_UPDATER_VERSION_V7, canPromptInteractively, checkCompatibilityCloud, checkPlanValidUpload, checkRemoteCliMessages, createSupabaseClient, deletedFailedVersion, findRoot, findSavedKey, formatError, getAppId, getBundleVersion, getCapgoPluginTags, getCompatibilityDetails, getConfig, getInstalledVersion, getLocalConfig, getLocalDependencies, getOrganizationId, getPMAndCommand, getRemoteChecksums, getRemoteFileConfig, hasCliPermission, hasOrganizationPerm, isCompatible, isDeprecatedPluginVersion, OrganizationPerm, regexSemver, resolveUserIdFromApiKey, sendEvent, updateConfigUpdater, updateOrCreateChannel, updateOrCreateVersion, UPLOAD_TIMEOUT, uploadTUS, uploadUrl, zipFile } from '../utils'
import { getVersionSuggestions, interactiveVersionBump } from '../versionHelpers'
import { maybePromptBuilderCta, shouldBlockIncompatibleUpload } from './builder-cta'
import { checkIndexPosition, searchInDirectory } from './check'
Expand Down Expand Up @@ -1459,6 +1459,7 @@ export async function uploadBundleInternal(preAppid: string, options: OptionsUpl
tags: {
'app-id': appid,
'bundle': bundle,
...getCapgoPluginTags(options.packageJson),
},
notify: false,
}, options.verbose)
Expand All @@ -1472,6 +1473,7 @@ export async function uploadBundleInternal(preAppid: string, options: OptionsUpl
tags: {
'app-id': appid,
'bundle': bundle,
...getCapgoPluginTags(options.packageJson),
},
notify: false,
notifyConsole: true,
Expand Down
30 changes: 27 additions & 3 deletions cli/src/init/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { copyToClipboard, revealInFinder } from '../support/clipboard'
import { appendInternalLog, getInternalLogPath, startInternalLog } from '../support/internal-log'
import { showReplicationProgress } from '../replicationProgress'
import { formatRunnerCommand, splitRunnerCommand } from '../runner-command'
import { createSupabaseClient, defaultApiHost, findBuildCommandForProjectType, findMainFile, findMainFileForProjectType, findProjectType, findRoot, findSavedKey, findSavedKeySilent, formatError, getAllPackagesDependencies, getAppId, getBundleVersion, getConfig, getLocalConfig, getNativeProjectResetAdvice, getOrganizationListWithPermission, getPackageScripts, getPMAndCommand, hasCliPermission, PACKNAME, projectIsMonorepo, resolveUserIdFromApiKey, updateConfigbyKey, updateConfigUpdater, validateIosUpdaterSync } from '../utils'
import { createSupabaseClient, defaultApiHost, findBuildCommandForProjectType, findMainFile, findMainFileForProjectType, findProjectType, findRoot, findSavedKey, findSavedKeySilent, formatError, getAllPackagesDependencies, getAppId, getBundleVersion, getCapgoPluginTags, getConfig, getLocalConfig, getNativeProjectResetAdvice, getOrganizationListWithPermission, getPackageScripts, getPMAndCommand, hasCliPermission, PACKNAME, projectIsMonorepo, resolveUserIdFromApiKey, updateConfigbyKey, updateConfigUpdater, validateIosUpdaterSync } from '../utils'
import { buildAppIdConflictSuggestions, isAppAlreadyExistsError } from './app-conflict'
import { cancel as pCancel, confirm as pConfirm, intro as pIntro, isCancel as pIsCancel, log as pLog, outro as pOutro, select as pSelect, spinner as pSpinner, text as pText } from './prompts'
import { appendInitStreamingLine, clearInitStreamingOutput, setInitCodeDiff, setInitEncryptionSummary, setInitVersionWarning, startInitStreamingOutput, stopInitInkSession, updateInitStreamingStatus } from './runtime'
Expand Down Expand Up @@ -552,7 +552,11 @@ async function runInitDoctorDiagnostics(): Promise<void> {
}

async function exitCanceledInitOnboarding(orgId: string, apikey: string, message = 'You can resume the onboarding anytime by running the same command again'): Promise<never> {
await markSnag('onboarding-v2', orgId, apikey, 'canceled', undefined, '🤷')
await markSnag('onboarding-v2', orgId, apikey, 'canceled', undefined, '🤷', {
last_step: lastStepName,
elapsed_ms: Date.now() - initStartedAt,
...getCapgoPluginTags(globalPathToPackageJson),
})
pOutro(`Bye 👋\n💡 ${message}`)
exit(1)
}
Expand Down Expand Up @@ -1371,8 +1375,27 @@ async function warnIfNotInCapacitorRoot() {
}
}

// Onboarding telemetry context: step timing plus the last reached step so the
// 'canceled' event can report where and how late users abort.
let initStartedAt = Date.now()
let lastStepStartedAt = initStartedAt
let lastStepName = 'init-start'

function resetInitTelemetryContext() {
initStartedAt = Date.now()
lastStepStartedAt = initStartedAt
lastStepName = 'init-start'
}

async function markStep(orgId: string, apikey: string, step: string, appId: string) {
return markSnag('onboarding-v2', orgId, apikey, `onboarding-step-${step}`, appId)
const now = Date.now()
const elapsedMs = now - lastStepStartedAt
lastStepStartedAt = now
lastStepName = step
return markSnag('onboarding-v2', orgId, apikey, `onboarding-step-${step}`, appId, '✅', {
elapsed_ms: elapsedMs,
...getCapgoPluginTags(globalPathToPackageJson),
})
}

/**
Expand Down Expand Up @@ -4291,6 +4314,7 @@ async function maybeStarCapgoRepo(includeSkillsRepository = false, repository?:
}

export async function initApp(apikeyCommand: string, appId: string, options: SuperOptions) {
resetInitTelemetryContext()
globalSupaHost = options.supaHost // honor --supa-host for the support-logs upload
const pm = getPMAndCommand()
// Start the verbose internal log early so it captures the whole run (incl.
Expand Down
51 changes: 51 additions & 0 deletions cli/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,57 @@ export function getBundleVersion(f: string = findRoot(cwd()), file: string | und
return packageJson.version ?? ''
}

// Cached so analytics call sites read package.json at most once per process.
let cachedCapgoPackages: string[] | undefined

/**
* List the @capgo/* packages declared in the project's package.json files
* (dependencies + devDependencies), sorted alphabetically.
* Used for telemetry only: never throws, returns [] when nothing is readable.
* The first call wins the cache, later calls reuse it regardless of path.
*/
export function listCapgoPackages(packageJsonPath?: string): string[] {
if (cachedCapgoPackages)
return cachedCapgoPackages
const found = new Set<string>()
try {
const files = packageJsonPath
? packageJsonPath.split(',').map(file => file.trim()).filter(Boolean)
: [join(findRoot(cwd()), PACKNAME)]
for (const file of files) {
try {
if (!existsSync(file))
continue
const pkg = JSON.parse(readFileSync(file, 'utf-8'))
const dependencies = [...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})]
for (const dependency of dependencies) {
if (dependency.startsWith('@capgo/'))
found.add(dependency)
}
}
catch {
// Unreadable or invalid package.json: skip it, telemetry must not break commands.
}
}
}
catch {
// Root resolution failed: report no plugins instead of throwing.
}
cachedCapgoPackages = [...found].sort()
return cachedCapgoPackages
}

/**
* Plugin tags shared by key analytics events (init steps, doctor, upload).
*/
export function getCapgoPluginTags(packageJsonPath?: string): { capgo_plugins: string, capgo_plugin_count: number } {
const plugins = listCapgoPackages(packageJsonPath)
return {
capgo_plugins: plugins.join(','),
capgo_plugin_count: plugins.length,
}
}
Comment on lines +257 to +306

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cache key ignores packageJsonPath, so telemetry can become path-order dependent.

After the first call, listCapgoPackages(...) always returns the first cached result and ignores later packageJsonPath values. That breaks the path-specific contract expected by callers (getCapgoPluginTags(options.packageJson) in Doctor/Upload) and can attach plugin tags from the wrong manifest set in long-lived processes.

💡 Suggested fix (cache by normalized path set)
-// Cached so analytics call sites read package.json at most once per process.
-let cachedCapgoPackages: string[] | undefined
+// Cache per normalized package.json path set.
+const cachedCapgoPackagesByPath = new Map<string, string[]>()

 export function listCapgoPackages(packageJsonPath?: string): string[] {
-  if (cachedCapgoPackages)
-    return cachedCapgoPackages
-  const found = new Set<string>()
+  const files = packageJsonPath
+    ? packageJsonPath.split(',').map(file => file.trim()).filter(Boolean)
+    : [join(findRoot(cwd()), PACKNAME)]
+  const normalizedFiles = files.map(file => resolve(file)).sort((a, b) => a.localeCompare(b))
+  const cacheKey = normalizedFiles.join(',')
+  const cached = cachedCapgoPackagesByPath.get(cacheKey)
+  if (cached)
+    return cached
+
+  const found = new Set<string>()

   try {
-    const files = packageJsonPath
-      ? packageJsonPath.split(',').map(file => file.trim()).filter(Boolean)
-      : [join(findRoot(cwd()), PACKNAME)]
     for (const file of files) {
       try {
         if (!existsSync(file))
           continue
         const pkg = JSON.parse(readFileSync(file, 'utf-8'))
@@
   catch {
     // Root resolution failed: report no plugins instead of throwing.
   }
-  cachedCapgoPackages = [...found].sort()
-  return cachedCapgoPackages
+  const result = [...found].sort()
+  cachedCapgoPackagesByPath.set(cacheKey, result)
+  return result
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/src/utils.ts` around lines 257 - 306, The cachedCapgoPackages global
currently ignores packageJsonPath so listCapgoPackages(packageJsonPath) always
returns the first result; change the cache to key by the normalized
packageJsonPath set (e.g., use a Map keyed by a deterministic string like a
sorted, comma-joined absolute paths or single path) and adjust listCapgoPackages
to compute that key from packageJsonPath (normalizing, splitting, trimming,
resolving to absolute paths) before checking/setting the cache; update
cachedCapgoPackages to the new Map type and ensure getCapgoPluginTags still
calls listCapgoPackages(packageJsonPath) unchanged so telemetry respects the
provided path(s).


function returnVersion(version: string) {
const tmpVersion = version.replace('^', '').replace('~', '')
if (canParse(tmpVersion)) {
Expand Down
28 changes: 28 additions & 0 deletions cli/test/test-doctor-analytics.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#!/usr/bin/env node
import assert from 'node:assert/strict'
import { mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { computeDoctorAnalyticsTags } from '../src/app/info.ts'
import { getCapgoPluginTags, listCapgoPackages } from '../src/utils.ts'

console.log('🧪 Testing doctor analytics tags...\n')

Expand Down Expand Up @@ -31,4 +35,28 @@ assert.equal(allOutdated.is_outdated, true)
assert.equal(allOutdated.dependency_count, 2)
assert.equal(allOutdated.outdated_count, 2)

// --- capgo plugin tags helper (shared by init/doctor/upload events) ---
const dir = mkdtempSync(join(tmpdir(), 'capgo-plugin-tags-'))
const pkgPath = join(dir, 'package.json')
writeFileSync(pkgPath, JSON.stringify({
dependencies: {
'@capgo/capacitor-updater': '^7.0.0',
'@capacitor/core': '^7.0.0',
},
devDependencies: {
'@capgo/cli': '^7.0.0',
'@capgo/capacitor-social-login': '^1.0.0',
},
}))

const plugins = listCapgoPackages(pkgPath)
assert.deepEqual(plugins, ['@capgo/capacitor-social-login', '@capgo/capacitor-updater', '@capgo/cli'], 'deps + devDeps, sorted, @capgo/* only')

const pluginTags = getCapgoPluginTags(pkgPath)
assert.equal(pluginTags.capgo_plugins, '@capgo/capacitor-social-login,@capgo/capacitor-updater,@capgo/cli')
assert.equal(pluginTags.capgo_plugin_count, 3)

// The result is cached per process: later calls reuse it whatever the path.
assert.deepEqual(listCapgoPackages('/nonexistent/package.json'), plugins)

console.log('✅ doctor analytics tags tests passed')
13 changes: 13 additions & 0 deletions cli/test/test-v2-event-migration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ try {
assert.equal(body.user_id, undefined, 'CLI must not send user_id (backend derives the actor from the key)')
assert.deepEqual(body.tags, { 'app-id': 'com.example.app' })

// The optional tags parameter merges caller tags with the app-id tag.
await markSnag('onboarding-v2', 'org-123', 'capgo-key', 'canceled', undefined, '🤷', {
last_step: 'add-app',
elapsed_ms: 1234,
})

const eventRequests = requests.filter(request => request.url.endsWith('/private/events'))
assert.equal(eventRequests.length, 2, 'Expected one request per markSnag call')
const canceledBody = JSON.parse(eventRequests[1].init.body)
assert.equal(canceledBody.event, 'canceled')
assert.equal(canceledBody.icon, '🤷')
assert.deepEqual(canceledBody.tags, { last_step: 'add-app', elapsed_ms: 1234 })

console.log('✅ v2 event migration tests passed')
}
finally {
Expand Down
1 change: 1 addition & 0 deletions read_replicate/schema_replicate.sql
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
stats_refresh_requested_at timestamp without time zone,
build_timeout_seconds bigint DEFAULT 900 NOT NULL,
build_timeout_updated_at timestamp with time zone DEFAULT now() NOT NULL,
first_update_delivered_at timestamp with time zone,
CONSTRAINT apps_build_timeout_seconds_check CHECK (((build_timeout_seconds >= 300) AND (build_timeout_seconds <= 21600)))
);

Expand Down Expand Up @@ -364,7 +365,7 @@
onboarding jsonb DEFAULT '{"intent": "unknown"}'::jsonb NOT NULL,
CONSTRAINT orgs_max_apikey_expiration_days_valid CHECK (((max_apikey_expiration_days IS NULL) OR ((max_apikey_expiration_days >= 1) AND (max_apikey_expiration_days <= 365)))),
CONSTRAINT orgs_onboarding_valid CHECK (((jsonb_typeof(onboarding) = 'object'::text) AND ((NOT (onboarding ? 'intent'::text)) OR ((onboarding ->> 'intent'::text) = ANY (ARRAY['unknown'::text, 'ota'::text, 'builder'::text, 'both'::text, 'exploring'::text]))))),
CONSTRAINT orgs_password_policy_config_min_length_check CHECK (((password_policy_config IS NULL) OR ((jsonb_typeof(password_policy_config) = 'object'::text) AND ((NOT (password_policy_config ? 'min_length'::text)) OR ((jsonb_typeof((password_policy_config -> 'min_length'::text)) = 'number'::text) AND (((password_policy_config ->> 'min_length'::text))::numeric = trunc(((password_policy_config ->> 'min_length'::text))::numeric)) AND ((((password_policy_config ->> 'min_length'::text))::numeric >= (6)::numeric) AND (((password_policy_config ->> 'min_length'::text))::numeric <= (72)::numeric))))))),

Check failure on line 368 in read_replicate/schema_replicate.sql

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal 6 times.

See more on https://sonarcloud.io/project/issues?id=Cap-go_capgo&issues=AZ66M9ivvxKY-zkopyr9&open=AZ66M9ivvxKY-zkopyr9&pullRequest=2491
CONSTRAINT orgs_required_encryption_key_valid CHECK (((required_encryption_key IS NULL) OR (length((required_encryption_key)::text) = ANY (ARRAY[20, 21]))))
);

Expand Down
2 changes: 2 additions & 0 deletions src/components/dashboard/StepsApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import IconLoader from '~icons/lucide/loader-2'
import InviteTeammateModal from '~/components/dashboard/InviteTeammateModal.vue'
import { createDefaultApiKey, findUsablePlainApiKey } from '~/services/apikeys'
import { stepElapsed } from '~/services/onboardingTimer'
import { pushEvent } from '~/services/posthog'
import { getLocalConfig, isLocal, useSupabase } from '~/services/supabase'
import { sendEvent } from '~/services/tracking'
Expand Down Expand Up @@ -93,6 +94,7 @@
org_id: orgId,
tracking_version: 2,
notify: false,
tags: { step_elapsed_ms: stepElapsed() },
}).catch()
pushEvent(`user:onboarding-step-${stepToName(step.value)}`, config.supaHost, { org_id: orgId })
}
Expand Down Expand Up @@ -315,7 +317,7 @@

clearWatchers()

pollTimer.value = window.setInterval(async () => {

Check warning on line 320 in src/components/dashboard/StepsApp.vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=Cap-go_capgo&issues=AZ64YArOZBZf2NUk520d&open=AZ64YArOZBZf2NUk520d&pullRequest=2491
try {
const current = await getAppsCount()
if (initialCount.value !== null && current > initialCount.value) {
Expand Down
2 changes: 2 additions & 0 deletions src/components/dashboard/StepsBuild.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import IconAndroid from '~icons/mdi/android'
import IconApple from '~icons/mdi/apple'
import { createDefaultApiKey, findUsablePlainApiKey } from '~/services/apikeys'
import { stepElapsed } from '~/services/onboardingTimer'
import { pushEvent } from '~/services/posthog'
import { getLocalConfig, isLocal, useSupabase } from '~/services/supabase'
import { sendEvent } from '~/services/tracking'
Expand Down Expand Up @@ -150,6 +151,7 @@
org_id: orgId,
tracking_version: 2,
notify: false,
tags: { step_elapsed_ms: stepElapsed() },
}).catch()
pushEvent(`user:onboarding-build-${stepToName(step.value)}`, config.supaHost, { org_id: orgId })
}
Expand Down Expand Up @@ -307,7 +309,7 @@

clearWatchers()

pollTimer.value = window.setInterval(async () => {

Check warning on line 312 in src/components/dashboard/StepsBuild.vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=Cap-go_capgo&issues=AZ64YArkZBZf2NUk520e&open=AZ64YArkZBZf2NUk520e&pullRequest=2491
try {
const current = await getBuildRequestsCount(platform)
if (initialCount.value !== null && current > initialCount.value) {
Expand Down
2 changes: 2 additions & 0 deletions src/components/dashboard/StepsBundle.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import IconLoader from '~icons/lucide/loader-2'
import InviteTeammateModal from '~/components/dashboard/InviteTeammateModal.vue'
import { createDefaultApiKey, findUsablePlainApiKey } from '~/services/apikeys'
import { stepElapsed } from '~/services/onboardingTimer'
import { pushEvent } from '~/services/posthog'
import { getLocalConfig, isLocal, useSupabase } from '~/services/supabase'
import { sendEvent } from '~/services/tracking'
Expand Down Expand Up @@ -80,6 +81,7 @@
org_id: orgId,
tracking_version: 2,
notify: false,
tags: { step_elapsed_ms: stepElapsed() },
}).catch()
pushEvent(`user:onboarding-bundle-${stepToName(step.value)}`, config.supaHost, { org_id: orgId })
}
Expand Down Expand Up @@ -242,7 +244,7 @@

clearWatchers()

pollTimer.value = window.setInterval(async () => {

Check warning on line 247 in src/components/dashboard/StepsBundle.vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=Cap-go_capgo&issues=AZ64YAr5ZBZf2NUk520f&open=AZ64YAr5ZBZf2NUk520f&pullRequest=2491
try {
const current = await getVersionsCount()
if (initialCount.value !== null && current > initialCount.value) {
Expand Down
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { setupLayouts } from 'virtual:generated-layouts'
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from 'vue-router/auto-routes'
import { captureFirstTouch } from '~/services/attribution'
import { installDeepLinkHandler } from '~/services/deepLinks'
import { getNativeExternalPurchaseRedirect, isNativeAppStoreContext, isNativeExternalPurchaseRestrictedPath } from '~/services/nativeCompliance'
import { posthogLoader } from '~/services/posthog'
Expand Down Expand Up @@ -192,6 +193,8 @@ router.beforeEach((to, from, next) => {

const config = getLocalConfig()
posthogLoader(config.supaHost)
// Capture first-touch attribution (UTM params, referrer) before any navigation strips them
captureFirstTouch()

// install all modules under `modules/`
type UserModule = (ctx: { app: typeof app, router: Router }) => void
Expand Down
12 changes: 11 additions & 1 deletion src/pages/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
import iconPassword from '~icons/ph/key?raw'
import mfaIcon from '~icons/simple-icons/2fas?raw'
import { hideLoader } from '~/services/loader'
import { autoAuth, defaultApiHost, hashEmail, useSupabase } from '~/services/supabase'
import { pushEvent } from '~/services/posthog'
import { autoAuth, defaultApiHost, getLocalConfig, hashEmail, useSupabase } from '~/services/supabase'
import { openSupport } from '~/services/support'

const route = useRoute('/login')
const supabase = useSupabase()
const config = getLocalConfig()
const isLoading = ref(false)
const isMobile = ref(Capacitor.isNativePlatform())
const turnstileToken = ref('')
Expand Down Expand Up @@ -296,6 +298,11 @@
isLoading.value = false
console.error('error', error)
setErrors('login-account', [error.message], {})
// Coarse failure reason for analytics; never include credentials
const reason = error.message.includes('Invalid login credentials')
? 'invalid_credentials'
: error.message.includes('captcha') ? 'captcha' : 'unknown'

Check warning on line 304 in src/pages/login.vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=Cap-go_capgo&issues=AZ64YAqOZBZf2NUk520Q&open=AZ64YAqOZBZf2NUk520Q&pullRequest=2491
pushEvent('user:login-failed', config.supaHost, { reason })
if (error.message.includes('Invalid login credentials')) {
turnstileToken.value = ''
captchaComponent.value?.reset()
Expand Down Expand Up @@ -389,6 +396,7 @@

if (error) {
console.error('SSO login error', error)
pushEvent('user:login-failed', config.supaHost, { reason: error.message.includes('captcha') ? 'captcha' : 'sso_failed' })
turnstileToken.value = ''
captchaComponent.value?.reset()
if (error.message.includes('captcha')) {
Expand All @@ -407,6 +415,7 @@
}
catch (err) {
console.error('SSO login error', err)
pushEvent('user:login-failed', config.supaHost, { reason: 'sso_failed' })
turnstileToken.value = ''
captchaComponent.value?.reset()
toast.error(t('invalid-auth'))
Expand All @@ -423,6 +432,7 @@
})

if (verify.error) {
pushEvent('user:login-failed', config.supaHost, { reason: 'mfa_failed' })
toast.error(t('invalid-mfa-code'))
console.error('verify error', verify.error)
isLoading.value = false
Expand Down
Loading
Loading