Skip to content

Analytics: add OAuth authentication and guest access events #9541

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 18, 2025
Merged
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
2 changes: 1 addition & 1 deletion dev/prod/.env
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ COLLABORATOR_URL=ws://locahost:3078
PRINT_URL=http://localhost:4005
SIGN_URL=http://localhost:4006

ANALYTICS_COLLECTOR_URL=http://localhost:4007
ANALYTICS_COLLECTOR_URL=http://huly.local:4017
AI_URL=http://localhost:4010
7 changes: 4 additions & 3 deletions packages/analytics-providers/src/analyticsCollector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ export class AnalyticsCollectorProvider implements AnalyticProvider {
)
}

setWorkspace (ws: string): void {
setWorkspace (ws: string, guest: boolean): void {
const prop: string = guest ? 'visited-workspace' : 'workspace'
this.addEvent(
AnalyticEventType.SetGroup,
{
Expand All @@ -224,8 +225,8 @@ export class AnalyticsCollectorProvider implements AnalyticProvider {
name: ws,
joined_at: new Date().toISOString()
},
$group_type: 'workspace',
$groups: { workspace: ws }
$group_type: prop,
$groups: { [prop]: ws }
},
'$groupidentify'
)
Expand Down
9 changes: 4 additions & 5 deletions packages/analytics-providers/src/posthog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,10 @@ export class PosthogAnalyticProvider implements AnalyticProvider {
posthog.setPersonProperties({ [key]: value })
}

setWorkspace (ws: string): void {
this.setTag('workspace', ws)
posthog.group('workspace', ws, {
name: `${ws}`
})
setWorkspace (ws: string, guest: boolean): void {
const prop: string = guest ? 'visited-workspace' : 'workspace'
this.setTag(prop, ws)
if (!guest) posthog.group(prop, ws, { name: `${ws}` })
}

logout (): void {
Expand Down
5 changes: 3 additions & 2 deletions packages/analytics-providers/src/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ export class SentryAnalyticProvider implements AnalyticProvider {
Sentry.setTag(key, value)
}

setWorkspace (ws: string): void {
this.setTag('workspace', ws)
setWorkspace (ws: string, guest: boolean): void {
const prop: string = guest ? 'visited-workspace' : 'workspace'
this.setTag(prop, ws)
}

handleEvent (event: string): void {
Expand Down
53 changes: 52 additions & 1 deletion packages/analytics-providers/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,21 @@
import { UAParser } from 'ua-parser-js'
import { getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { desktopPlatform } from '@hcengineering/ui'
import { desktopPlatform, getCurrentLocation } from '@hcengineering/ui'
import { Analytics } from '@hcengineering/analytics'

const parser = UAParser()

let _isSignUp: boolean = false
export const signupStore = {
setSignUpFlow: (isSignUp: boolean) => {
_isSignUp = isSignUp
},
getSignUpFlow: () => {
return _isSignUp
}
}

function getUrlTrackingParams (): Record<string, string | null> {
const params = new URLSearchParams(window.location.search)
return {
Expand Down Expand Up @@ -102,3 +113,43 @@ export function collectEventMetadata (properties: Record<string, any> = {}): Rec
...trackingParams
}
}

function getProviderFromUrl (): string | null {
const location = getCurrentLocation()
const referrer = document.referrer

if (referrer !== '') {
try {
const referrerUrl = new URL(referrer)
const hostname = referrerUrl.hostname

if (hostname === 'accounts.google.com') {
return 'google'
} else if (hostname === 'github.com') {
return 'github'
}
} catch (error) {
// Invalid URL, ignored
}
}

if (location.query?.provider != null && location.query.provider !== '') {
return location.query.provider
}

return null
}

export function trackOAuthCompletion (result: any): void {
const provider = getProviderFromUrl()
if (provider == null) return

const isSignUp = signupStore.getSignUpFlow()
const success = result != null

const eventPrefix = isSignUp ? 'signup' : 'login'
const eventSuffix = success ? 'completed' : 'error'
const eventName: string = `${eventPrefix}.${provider}.${eventSuffix}`

Analytics.handleEvent(eventName)
}
5 changes: 3 additions & 2 deletions packages/analytics-service/src/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ export class SentryAnalyticProvider implements AnalyticProvider {
Sentry.setTag(key, value)
}

setWorkspace (ws: string): void {
this.setTag('workspace', ws)
setWorkspace (ws: string, guest: boolean): void {
const prop: string = guest ? 'visited-workspace' : 'workspace'
this.setTag(prop, ws)
}

handleEvent (event: string): void {
Expand Down
6 changes: 3 additions & 3 deletions packages/analytics/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface AnalyticProvider {
setUser: (email: string, data: any) => void
setAlias: (distinctId: string, alias: string) => void
setTag: (key: string, value: string) => void
setWorkspace: (ws: string) => void
setWorkspace: (ws: string, guest: boolean) => void
handleEvent: (event: string, params: Record<string, string>) => void
handleError: (error: Error) => void
navigate: (path: string) => void
Expand Down Expand Up @@ -45,9 +45,9 @@ export const Analytics = {
})
},

setWorkspace (ws: string): void {
setWorkspace (ws: string, guest: boolean): void {
providers.forEach((provider) => {
provider.setWorkspace(ws)
provider.setWorkspace(ws, guest)
})
},

Expand Down
13 changes: 9 additions & 4 deletions plugins/guest-resources/src/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,6 @@ export async function connect (title: string): Promise<Client | undefined> {
}
})
console.log('logging in as guest')
Analytics.handleEvent('GUEST LOGIN')
// Analytics.setWorkspace(wsUrl)

const account = workspaceLoginInfo.account

Expand All @@ -191,9 +189,16 @@ export async function connect (title: string): Promise<Client | undefined> {
fullSocialIds: []
}

const data: Record<string, any> = {
guest_uuid: account,
visited_workspace: wsUrl,
visited_workspace_uuid: workspaceLoginInfo.workspace
}
Analytics.handleEvent('GUEST LOGIN', data)

if (me !== undefined) {
Analytics.setUser(account, { account })
Analytics.setWorkspace(wsUrl)
Analytics.setUser(data.guest_uuid, data)
Analytics.setWorkspace(wsUrl, true)
console.log('login: employee account', me)
setCurrentAccount(me)
setCurrentEmployee('' as Ref<Employee>)
Expand Down
3 changes: 2 additions & 1 deletion plugins/login-resources/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@hcengineering/setting": "^0.6.17",
"@hcengineering/theme": "^0.6.5",
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/account-client": "^0.6.0"
"@hcengineering/account-client": "^0.6.0",
"@hcengineering/analytics-providers": "^0.6.0"
}
}
4 changes: 4 additions & 0 deletions plugins/login-resources/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export const LoginEvents = {
LoginGoogle: 'login.viaGoogle',
LoginGithub: 'login.viaGitHub',

LoginGuestStarted: 'login.guest.started',
LoginGuestCompleted: 'login.guest.completed',
LoginGuestError: 'login.guest.error',

CreateWorkspace: 'onboard.createWorkspace',
SelectWorkspace: 'onboard.selectWorkspace'
}
3 changes: 3 additions & 0 deletions plugins/login-resources/src/components/Auth.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { Loading } from '@hcengineering/ui'
import { logIn } from '@hcengineering/workbench'
import { trackOAuthCompletion } from '@hcengineering/analytics-providers'
import { onMount } from 'svelte'

import {
Expand All @@ -21,6 +22,8 @@

const result = await getLoginInfoFromQuery()

trackOAuthCompletion(result)

if (result != null) {
await logIn(result)

Expand Down
6 changes: 4 additions & 2 deletions plugins/login-resources/src/components/Join.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,18 @@
import Form from './Form.svelte'

import { Analytics } from '@hcengineering/analytics'
import { signupStore } from '@hcengineering/analytics-providers'
import { logIn, workbenchId } from '@hcengineering/workbench'
import { onMount } from 'svelte'
import { BottomAction } from '..'
import { loginAction, recoveryAction } from '../actions'
import login from '../plugin'

const location = getCurrentLocation()
Analytics.handleEvent('invite_link_activated')
Analytics.handleEvent('invite_link_activated', { invite_id: location.query?.inviteId })
let page = 'signUp'

$: signupStore.setSignUpFlow(page === 'signUp')

$: fields =
page === 'login'
? [
Expand Down
8 changes: 7 additions & 1 deletion plugins/login-resources/src/components/LoginForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
-->
<script lang="ts">
import { type IntlString, Severity, Status } from '@hcengineering/platform'
import { signupStore } from '@hcengineering/analytics-providers'
import { onMount } from 'svelte'

import { type BottomAction, doLoginAsGuest, doLoginNavigate, LoginMethods } from '../index'
import LoginPasswordForm from './LoginPasswordForm.svelte'
Expand All @@ -33,6 +35,10 @@

let method: LoginMethods = useOTP ? LoginMethods.Otp : LoginMethods.Password

onMount(() => {
signupStore.setSignUpFlow(false)
})

function changeMethod (event: CustomEvent<LoginMethods>): void {
method = event.detail
}
Expand All @@ -51,7 +57,7 @@
}
}

async function guestLogin () {
async function guestLogin (): Promise<void> {
let status = new Status(Severity.INFO, login.status.ConnectingToServer, {})
const [loginStatus, result] = await doLoginAsGuest()
status = loginStatus
Expand Down
18 changes: 17 additions & 1 deletion plugins/login-resources/src/components/Providers.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { getMetadata } from '@hcengineering/platform'
import { type ProviderInfo } from '@hcengineering/account-client'
import { AnySvelteComponent, Button, Grid, deviceOptionsStore, getCurrentLocation } from '@hcengineering/ui'
import { Analytics } from '@hcengineering/analytics'
import { onMount } from 'svelte'
import login from '../plugin'
import { getProviders } from '../utils'
Expand Down Expand Up @@ -60,13 +61,28 @@

return concatLink(accountsUrl, path)
}

function handleProviderClick (provider: Provider): void {
const currentPath = location.path[1]
const isSignUp = currentPath === 'signup'
const isJoin = currentPath === 'join'
const eventPrefix = isSignUp || isJoin ? 'signup' : 'login'
const eventName: string = `${eventPrefix}.${provider.name}.started`

Analytics.handleEvent(eventName)
}
</script>

{#if !$deviceOptionsStore.isMobile}
<div class="container">
<Grid column={getColumnsCount(enabledProviders.length)} columnGap={1} rowGap={1} alignItems={'center'}>
{#each enabledProviders as provider}
<a href={getLink(provider)}>
<a
href={getLink(provider)}
on:click={() => {
handleProviderClick(provider)
}}
>
<Button kind={'contrast'} shape={'round2'} size={'x-large'} width="100%" stopPropagation={false}>
<svelte:fragment slot="content">
<svelte:component this={provider.component} displayName={provider.displayName} />
Expand Down
8 changes: 7 additions & 1 deletion plugins/login-resources/src/components/SignupForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@
<script lang="ts">
import { OK, Severity, Status } from '@hcengineering/platform'
import { logIn } from '@hcengineering/workbench'
import { signupStore } from '@hcengineering/analytics-providers'

import BottomActionComponent from './BottomAction.svelte'
import login from '../plugin'
import { getPasswordValidationRules } from '../validations'
import { goTo } from '../utils'
import Form from './Form.svelte'
import { OtpLoginSteps, signUp, signUpOtp } from '../index'
import { OtpLoginSteps, signUp, signUpOtp, type BottomAction } from '../index'
import type { Field } from '../types'
import OtpForm from './OtpForm.svelte'
import { onMount } from 'svelte'

export let signUpDisabled = false
export let localLoginHidden = false
Expand Down Expand Up @@ -70,6 +72,10 @@
goTo('login')
}

onMount(() => {
signupStore.setSignUpFlow(true)
})

const action = {
i18n: login.string.SignUp,
func: async () => {
Expand Down
17 changes: 10 additions & 7 deletions plugins/login-resources/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,23 +102,23 @@ export async function doLogin (email: string, password: string): Promise<[Status

export async function doLoginAsGuest (): Promise<[Status, LoginInfo | null]> {
try {
Analytics.handleEvent(LoginEvents.LoginGuestStarted)
const accountClient = getAccountClient(null)

const loginInfo = await accountClient.loginAsGuest()

/* Analytics.handleEvent(LoginEvents.LoginPassword, { email, ok: true })
Analytics.setUser(email, loginInfo.account) */
Analytics.handleEvent(LoginEvents.LoginGuestCompleted)

return [OK, loginInfo]
} catch (err: any) {
if (err instanceof PlatformError) {
// Analytics.handleEvent(LoginEvents.LoginPassword, { email, ok: false })
Analytics.handleEvent(LoginEvents.LoginGuestError)
await handleStatusError('Login error', err.status)

return [err.status, null]
} else {
// Analytics.handleEvent(LoginEvents.LoginPassword, { email, ok: false })
// Analytics.handleError(err)
Analytics.handleEvent(LoginEvents.LoginGuestError)
Analytics.handleError(err)

return [unknownError(err), null]
}
Expand Down Expand Up @@ -188,7 +188,6 @@ export async function createWorkspace (
const workspaceLoginInfo = await getAccountClient(token).createWorkspace(workspaceName, region)

Analytics.handleEvent(LoginEvents.CreateWorkspace, { name: workspaceName, ok: true })
Analytics.setWorkspace(workspaceName)

return [OK, workspaceLoginInfo]
} catch (err: any) {
Expand Down Expand Up @@ -496,6 +495,10 @@ export function navigateToWorkspace (
}

setLoginInfo(loginInfo)
Analytics.handleEvent(LoginEvents.SelectWorkspace, {
workspace: workspaceUrl,
workspace_uuid: loginInfo.workspace
})

if (navigateUrl != null) {
try {
Expand Down Expand Up @@ -607,7 +610,7 @@ export async function getInviteLinkId (

const inviteLink = await getAccountClient(token).createInvite(exp, emailMask, limit, role)

Analytics.handleEvent('Get invite link')
Analytics.handleEvent('Get invite link', { invite_id: inviteLink })

return inviteLink
}
Expand Down
Loading