diff --git a/dev/prod/src/analytics/analyticsCollector.ts b/dev/prod/src/analytics/analyticsCollector.ts index 029a855545b..54b722f020a 100644 --- a/dev/prod/src/analytics/analyticsCollector.ts +++ b/dev/prod/src/analytics/analyticsCollector.ts @@ -21,7 +21,7 @@ import { AnalyticEvent, AnalyticEventType } from '@hcengineering/analytics-colle import { Config } from '../platform' export class AnalyticsCollectorProvider implements AnalyticProvider { - private readonly collectIntervalMs = 1000 + private readonly collectIntervalMs = 5000 private readonly events: AnalyticEvent[] = [] @@ -85,10 +85,10 @@ export class AnalyticsCollectorProvider implements AnalyticProvider { this.setTag('workspace', ws) } - handleEvent(event: string): void { + handleEvent(event: string, params: Record): void { this.events.push({ event: AnalyticEventType.CustomEvent, - params: { event }, + params: { ...params, event }, timestamp: Date.now() }) } @@ -108,4 +108,6 @@ export class AnalyticsCollectorProvider implements AnalyticProvider { timestamp: Date.now() }) } + + logout(): void {} } \ No newline at end of file diff --git a/dev/prod/src/analytics/posthog.ts b/dev/prod/src/analytics/posthog.ts index e5e12b49d28..dff093d8d7b 100644 --- a/dev/prod/src/analytics/posthog.ts +++ b/dev/prod/src/analytics/posthog.ts @@ -22,8 +22,11 @@ export class PosthogAnalyticProvider implements AnalyticProvider { name: `${ws}` }) } - handleEvent(event: string): void { - posthog.capture(event) + logout(): void { + posthog.reset() + } + handleEvent(event: string, params: Record): void { + posthog.capture(event, params) } handleError(error: Error): void { posthog.capture(error.message) diff --git a/dev/prod/src/analytics/sentry.ts b/dev/prod/src/analytics/sentry.ts index d4b14f9cd0b..c4464ab6087 100644 --- a/dev/prod/src/analytics/sentry.ts +++ b/dev/prod/src/analytics/sentry.ts @@ -30,6 +30,9 @@ export class SentryAnalyticProvider implements AnalyticProvider { setUser(email: string): void { Sentry.setUser({ email }) } + logout(): void { + Sentry.setUser(null) + } setTag(key: string, value: string): void { Sentry.setTag(key, value) } diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index f9604d7408c..4fc4ef12f6d 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -31,7 +31,7 @@ import view, { createAction, showColorsViewOption, actionTemplates as viewTempla import workbench, { createNavigateAction, type Application } from '@hcengineering/model-workbench' import notification from '@hcengineering/notification' import { type IntlString } from '@hcengineering/platform' -import { recruitId, type Applicant } from '@hcengineering/recruit' +import { recruitId, type Applicant, RecruitEvents } from '@hcengineering/recruit' import setting from '@hcengineering/setting' import { type KeyBinding, type ViewOptionModel, type ViewOptionsModel } from '@hcengineering/view' @@ -183,6 +183,7 @@ export function createModel (builder: Builder): void { _class: recruit.mixin.Candidate, icon: contact.icon.Person, label: recruit.string.Talents, + createEvent: RecruitEvents.PlusTalentButtonClicked, createLabel: recruit.string.TalentCreateLabel, createComponent: recruit.component.CreateCandidate, createComponentProps: { shouldSaveDraft: false } diff --git a/models/tracker/src/actions.ts b/models/tracker/src/actions.ts index f6b859b033d..36fd0261127 100644 --- a/models/tracker/src/actions.ts +++ b/models/tracker/src/actions.ts @@ -20,7 +20,7 @@ import task from '@hcengineering/model-task' import view, { actionTemplates, createAction } from '@hcengineering/model-view' import workbench, { createNavigateAction } from '@hcengineering/model-workbench' import { type IntlString } from '@hcengineering/platform' -import { trackerId } from '@hcengineering/tracker' +import { TrackerEvents, trackerId } from '@hcengineering/tracker' import { type KeyBinding, type ViewAction } from '@hcengineering/view' import tracker from './plugin' @@ -124,6 +124,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId: mode: ['context', 'browser'], group: 'edit' }, + analyticsEvent: TrackerEvents.ProjectArchived, override: [view.action.Archive, view.action.Delete] }, tracker.action.DeleteProject @@ -145,6 +146,7 @@ export function createActions (builder: Builder, issuesId: string, componentsId: mode: ['context', 'browser'], group: 'edit' }, + analyticsEvent: TrackerEvents.ProjectDeleted, override: [view.action.Archive, view.action.Delete] }, tracker.action.DeleteProjectClean @@ -187,7 +189,8 @@ export function createActions (builder: Builder, issuesId: string, componentsId: group: 'remove' }, visibilityTester: view.function.CanDeleteObject, - override: [view.action.Delete] + override: [view.action.Delete], + analyticsEvent: TrackerEvents.IssueDeleted }, tracker.action.DeleteIssue ) @@ -218,7 +221,8 @@ export function createActions (builder: Builder, issuesId: string, componentsId: application: tracker.app.Tracker, group: 'create' }, - override: [tracker.action.NewIssueGlobal] + override: [tracker.action.NewIssueGlobal], + analyticsEvent: TrackerEvents.NewIssueBindingCalled }, tracker.action.NewIssue ) @@ -239,7 +243,8 @@ export function createActions (builder: Builder, issuesId: string, componentsId: context: { mode: [], group: 'create' - } + }, + analyticsEvent: TrackerEvents.IssueCreateFromGlobalActionCalled }, tracker.action.NewIssueGlobal ) diff --git a/packages/analytics-service/src/sentry.ts b/packages/analytics-service/src/sentry.ts index 09a21f7b4e5..6b2e972bc90 100644 --- a/packages/analytics-service/src/sentry.ts +++ b/packages/analytics-service/src/sentry.ts @@ -46,4 +46,5 @@ export class SentryAnalyticProvider implements AnalyticProvider { } navigate (path: string): void {} + logout (): void {} } diff --git a/packages/analytics/src/index.ts b/packages/analytics/src/index.ts index 41e9aa3fcd5..99c26f450e4 100644 --- a/packages/analytics/src/index.ts +++ b/packages/analytics/src/index.ts @@ -10,9 +10,10 @@ export interface AnalyticProvider { setUser: (email: string) => void setTag: (key: string, value: string) => void setWorkspace: (ws: string) => void - handleEvent: (event: string) => void + handleEvent: (event: string, params: Record) => void handleError: (error: Error) => void navigate: (path: string) => void + logout: () => void } export const Analytics = { @@ -41,9 +42,9 @@ export const Analytics = { }) }, - handleEvent (event: string): void { + handleEvent (event: string, params: Record = {}): void { providers.forEach((provider) => { - provider.handleEvent(event) + provider.handleEvent(event, params) }) }, @@ -57,6 +58,12 @@ export const Analytics = { providers.forEach((provider) => { provider.navigate(path) }) + }, + + logout (): void { + providers.forEach((provider) => { + provider.logout() + }) } } diff --git a/packages/core/package.json b/packages/core/package.json index 173bad3a1ff..9cc0ae53462 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -39,6 +39,7 @@ }, "dependencies": { "@hcengineering/platform": "^0.6.11", + "@hcengineering/analytics": "^0.6.0", "fast-equals": "^5.0.1" }, "repository": "https://github.com/hcengineering/platform", diff --git a/packages/core/src/operations.ts b/packages/core/src/operations.ts index 4c70d569a7b..48a2dc416eb 100644 --- a/packages/core/src/operations.ts +++ b/packages/core/src/operations.ts @@ -26,6 +26,7 @@ import type { WithLookup } from './storage' import { DocumentClassQuery, Tx, TxApplyResult, TxCUD, TxFactory, TxProcessor } from './tx' +import { Analytics } from '@hcengineering/analytics' /** * @public @@ -541,7 +542,8 @@ export async function updateAttribute ( _class: Ref>, attribute: { key: string, attr: AnyAttribute }, value: any, - saveModified: boolean = false + saveModified: boolean = false, + analyticsProps: Record = {} ): Promise { const doc = object const attributeKey = attribute.key @@ -549,6 +551,13 @@ export async function updateAttribute ( const modifiedOn = saveModified ? doc.modifiedOn : Date.now() const modifiedBy = attribute.key === 'modifiedBy' ? value : saveModified ? doc.modifiedBy : undefined const attr = attribute.attr + + const baseAnalyticsProps = { + objectClass: _class, + objectId: object._id, + attribute: attributeKey, + ...analyticsProps + } if (client.getHierarchy().isMixin(attr.attributeOf)) { await client.updateMixin( doc._id, @@ -559,6 +568,7 @@ export async function updateAttribute ( modifiedOn, modifiedBy ) + Analytics.handleEvent('ChangeAttribute', { ...baseAnalyticsProps, value }) } else { if (client.getHierarchy().isDerived(attribute.attr.type._class, core.class.ArrOf)) { const oldValue: any[] = (object as any)[attributeKey] ?? [] @@ -568,6 +578,10 @@ export async function updateAttribute ( const toPush = val.filter((it) => !oldValue.includes(it)) if (toPull.length > 0) { await client.update(object, { $pull: { [attributeKey]: { $in: toPull } } }, false, modifiedOn, modifiedBy) + Analytics.handleEvent('RemoveCollectionItems', { + ...baseAnalyticsProps, + removed: toPull + }) } if (toPush.length > 0) { await client.update( @@ -577,9 +591,17 @@ export async function updateAttribute ( modifiedOn, modifiedBy ) + Analytics.handleEvent('AddCollectionItems', { + ...baseAnalyticsProps, + added: toPush + }) } } else { await client.update(object, { [attributeKey]: value }, false, modifiedOn, modifiedBy) + Analytics.handleEvent('SetCollectionItems', { + ...baseAnalyticsProps, + value + }) } } } diff --git a/packages/presentation/src/components/AttributeBarEditor.svelte b/packages/presentation/src/components/AttributeBarEditor.svelte index 2caa299a94d..07623f240a0 100644 --- a/packages/presentation/src/components/AttributeBarEditor.svelte +++ b/packages/presentation/src/components/AttributeBarEditor.svelte @@ -20,6 +20,7 @@ import { createEventDispatcher } from 'svelte' import { getAttribute, KeyedAttribute, updateAttribute } from '../attributes' import { getAttributeEditor, getClient } from '../utils' + import { Analytics } from '@hcengineering/analytics' export let key: KeyedAttribute | string export let object: Doc | Record @@ -29,6 +30,7 @@ export let showHeader: boolean = true export let readonly = false export let draft = false + export let identifier: string | undefined = undefined export let kind: ButtonKind = 'link' export let size: ButtonSize = 'large' @@ -48,7 +50,9 @@ if (draft) { ;(doc as any)[attributeKey] = value } else { - void updateAttribute(client, doc, doc._class, { key: attributeKey, attr: attribute }, value) + void updateAttribute(client, doc, doc._class, { key: attributeKey, attr: attribute }, value, false, { + objectId: identifier ?? doc._id + }) } } diff --git a/packages/ui/src/components/Button.svelte b/packages/ui/src/components/Button.svelte index 9c44f0692d2..2627178856d 100644 --- a/packages/ui/src/components/Button.svelte +++ b/packages/ui/src/components/Button.svelte @@ -30,6 +30,7 @@ import Icon from './Icon.svelte' import Label from './Label.svelte' import Spinner from './Spinner.svelte' + import { Analytics } from '@hcengineering/analytics' export let label: IntlString | undefined = undefined export let labelParams: Record = {} @@ -68,6 +69,7 @@ export let adaptiveShrink: WidthType | null = null export let gap: 'medium' | 'large' = 'medium' export let stopPropagation: boolean = true + export let event: string | undefined = undefined $: iconSize = iconProps?.size !== undefined ? iconProps.size : size && size === 'inline' ? 'inline' : 'small' $: iconRightSize = iconRightProps?.size !== undefined ? iconRightProps.size : 'x-small' @@ -149,6 +151,11 @@ {title} type={kind === 'primary' ? 'submit' : 'button'} on:click={preventHandler} + on:click={() => { + if (event) { + Analytics.handleEvent(event) + } + }} on:click on:focus on:blur diff --git a/plugins/attachment-resources/package.json b/plugins/attachment-resources/package.json index 6cc3588ae9c..89da410105b 100644 --- a/plugins/attachment-resources/package.json +++ b/plugins/attachment-resources/package.json @@ -38,6 +38,7 @@ "typescript": "^5.3.3" }, "dependencies": { + "@hcengineering/analytics": "^0.6.0", "@hcengineering/activity": "^0.6.0", "@hcengineering/attachment": "^0.6.14", "@hcengineering/contact": "^0.6.24", diff --git a/plugins/attachment-resources/src/components/AttachmentStyleBoxCollabEditor.svelte b/plugins/attachment-resources/src/components/AttachmentStyleBoxCollabEditor.svelte index dd465838771..a65b770905d 100644 --- a/plugins/attachment-resources/src/components/AttachmentStyleBoxCollabEditor.svelte +++ b/plugins/attachment-resources/src/components/AttachmentStyleBoxCollabEditor.svelte @@ -13,7 +13,7 @@ // limitations under the License. --> diff --git a/plugins/document-resources/src/components/NewDocumentHeader.svelte b/plugins/document-resources/src/components/NewDocumentHeader.svelte index 83fb5055616..e6e0a041ddc 100644 --- a/plugins/document-resources/src/components/NewDocumentHeader.svelte +++ b/plugins/document-resources/src/components/NewDocumentHeader.svelte @@ -25,6 +25,9 @@ showPopup } from '@hcengineering/ui' import { openDoc } from '@hcengineering/view-resources' + import { Analytics } from '@hcengineering/analytics' + import { DocumentEvents } from '@hcengineering/document' + import document from '../plugin' import { getDocumentIdFromFragment } from '../utils' import CreateDocument from './CreateDocument.svelte' @@ -53,6 +56,7 @@ $: parent = getDocumentIdFromFragment(currentFragment ?? '') async function newDocument (): Promise { + Analytics.handleEvent(DocumentEvents.CreateDocumentButtonClicked) showPopup(CreateDocument, { space: currentSpace, parent }, 'top', async (id) => { if (id !== undefined && id !== null) { const doc = await client.findOne(document.class.Document, { _id: id }) diff --git a/plugins/document-resources/src/components/navigator/TeamspaceSpacePresenter.svelte b/plugins/document-resources/src/components/navigator/TeamspaceSpacePresenter.svelte index 7a5ada63bb4..23dc3f7c5c7 100644 --- a/plugins/document-resources/src/components/navigator/TeamspaceSpacePresenter.svelte +++ b/plugins/document-resources/src/components/navigator/TeamspaceSpacePresenter.svelte @@ -14,7 +14,7 @@ --> diff --git a/plugins/document-resources/src/components/teamspace/CreateTeamspace.svelte b/plugins/document-resources/src/components/teamspace/CreateTeamspace.svelte index 4a5eb98f056..30727b249b7 100644 --- a/plugins/document-resources/src/components/teamspace/CreateTeamspace.svelte +++ b/plugins/document-resources/src/components/teamspace/CreateTeamspace.svelte @@ -27,7 +27,7 @@ getCurrentAccount, WithLookup } from '@hcengineering/core' - import document, { Teamspace } from '@hcengineering/document' + import document, { Teamspace, DocumentEvents } from '@hcengineering/document' import { Asset } from '@hcengineering/platform' import presentation, { Card, getClient, reduceCalls } from '@hcengineering/presentation' import { @@ -45,6 +45,7 @@ import view from '@hcengineering/view' import { IconPicker, SpaceTypeSelector } from '@hcengineering/view-resources' import { createEventDispatcher } from 'svelte' + import { Analytics } from '@hcengineering/analytics' import documentRes from '../../plugin' @@ -207,6 +208,7 @@ rolesAssignment ) + Analytics.handleEvent(DocumentEvents.TeamspaceCreated, { id: teamspaceId }) close(teamspaceId) } diff --git a/plugins/document/src/analytics.ts b/plugins/document/src/analytics.ts new file mode 100644 index 00000000000..ab49fdbc9c3 --- /dev/null +++ b/plugins/document/src/analytics.ts @@ -0,0 +1,8 @@ +export enum DocumentEvents { + CreateDocumentButtonClicked = 'document.CreateButtonClicked', + PlusDocumentButtonClicked = 'document.PlusButtonClicked', + DocumentCreated = 'document.Created', + TeamspaceCreated = 'document.CreatedTeamspace', + DocumentEdited = 'document.Edited', + DocumentOpened = 'document.Opened' +} diff --git a/plugins/document/src/index.ts b/plugins/document/src/index.ts index 0a8565158ab..1c712bf3c87 100644 --- a/plugins/document/src/index.ts +++ b/plugins/document/src/index.ts @@ -16,6 +16,7 @@ import { documentId, documentPlugin } from './plugin' export * from './types' +export * from './analytics' export { documentId } export default documentPlugin diff --git a/plugins/drive-resources/src/components/CreateDrive.svelte b/plugins/drive-resources/src/components/CreateDrive.svelte index 6d0204431f4..19316e610e8 100644 --- a/plugins/drive-resources/src/components/CreateDrive.svelte +++ b/plugins/drive-resources/src/components/CreateDrive.svelte @@ -28,12 +28,13 @@ getCurrentAccount, WithLookup } from '@hcengineering/core' - import { Drive } from '@hcengineering/drive' + import { Drive, DriveEvents } from '@hcengineering/drive' import presentation, { Card, getClient, reduceCalls } from '@hcengineering/presentation' import { EditBox, Label, Toggle } from '@hcengineering/ui' import { SpaceTypeSelector } from '@hcengineering/view-resources' import driveRes from '../plugin' + import { Analytics } from '@hcengineering/analytics' export let drive: Drive | undefined = undefined @@ -168,7 +169,7 @@ // Create space type's mixin with roles assignments await client.createMixin(driveId, driveRes.class.Drive, core.space.Space, spaceType.targetClass, rolesAssignment) - + Analytics.handleEvent(DriveEvents.DriveCreated, { id: driveId }) close(driveId) } diff --git a/plugins/drive-resources/src/components/CreateFolder.svelte b/plugins/drive-resources/src/components/CreateFolder.svelte index afd672707de..2094299cb29 100644 --- a/plugins/drive-resources/src/components/CreateFolder.svelte +++ b/plugins/drive-resources/src/components/CreateFolder.svelte @@ -16,7 +16,7 @@ --> diff --git a/plugins/recruit-resources/src/components/CreateVacancy.svelte b/plugins/recruit-resources/src/components/CreateVacancy.svelte index 71f22734f47..c8cc2c52ad0 100644 --- a/plugins/recruit-resources/src/components/CreateVacancy.svelte +++ b/plugins/recruit-resources/src/components/CreateVacancy.svelte @@ -29,7 +29,7 @@ } from '@hcengineering/core' import { getEmbeddedLabel } from '@hcengineering/platform' import { Card, InlineAttributeBar, MessageBox, createQuery, getClient } from '@hcengineering/presentation' - import { Vacancy as VacancyClass } from '@hcengineering/recruit' + import { RecruitEvents, Vacancy, Vacancy as VacancyClass } from '@hcengineering/recruit' import tags from '@hcengineering/tags' import task, { ProjectType, makeRank } from '@hcengineering/task' import { selectedTypeStore, typeStore } from '@hcengineering/task-resources' @@ -40,15 +40,15 @@ EditBox, FocusHandler, IconAttachment, - Label, - Toggle, createFocusManager, showPopup } from '@hcengineering/ui' import { createEventDispatcher } from 'svelte' import recruit from '../plugin' import Company from './icons/Company.svelte' - import Vacancy from './icons/Vacancy.svelte' + import VacancyIcon from './icons/Vacancy.svelte' + import { Analytics } from '@hcengineering/analytics' + import { getSequenceId } from '../utils' const dispatch = createEventDispatcher() @@ -220,26 +220,33 @@ } const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true) + const data: Data = { + ...vacancyData, + name, + description: template?.shortDescription ?? '', + fullDescription, + private: false, + archived: false, + number: (incResult as any).object.sequence, + company, + members, + autoJoin: typeType.autoJoin ?? false, + owners: [getCurrentAccount()._id], + type: typeId + } - const id = await client.createDoc( - recruit.class.Vacancy, - core.space.Space, - { - ...vacancyData, - name, - description: template?.shortDescription ?? '', - fullDescription, - private: false, - archived: false, - number: (incResult as any).object.sequence, - company, - members, - autoJoin: typeType.autoJoin ?? false, - owners: [getCurrentAccount()._id], - type: typeId - }, - objectId - ) + const id = await client.createDoc(recruit.class.Vacancy, core.space.Space, data, objectId) + + Analytics.handleEvent(RecruitEvents.VacancyCreated, { + id: getSequenceId({ + ...data, + _id: id, + _class: recruit.class.Vacancy, + space: core.space.Space, + modifiedOn: 0, + modifiedBy: getCurrentAccount()._id + }) + }) if (issueTemplates.length > 0) { for (const issueTemplate of issueTemplates) { @@ -316,7 +323,7 @@ {typeType?.name}
-
{ showPopup(CreateCandidate, { shouldSaveDraft: true }, 'top') + Analytics.handleEvent(RecruitEvents.NewTalentButtonClicked) } diff --git a/plugins/recruit-resources/src/components/review/CreateReview.svelte b/plugins/recruit-resources/src/components/review/CreateReview.svelte index 9f394ba543b..b74bef5342a 100644 --- a/plugins/recruit-resources/src/components/review/CreateReview.svelte +++ b/plugins/recruit-resources/src/components/review/CreateReview.svelte @@ -30,7 +30,7 @@ import { getResource, OK, Resource, Severity, Status } from '@hcengineering/platform' import { Card, getClient } from '@hcengineering/presentation' import { UserBox, UserBoxList } from '@hcengineering/contact-resources' - import type { Applicant, Candidate, Review } from '@hcengineering/recruit' + import { Applicant, Candidate, RecruitEvents, Review } from '@hcengineering/recruit' import task from '@hcengineering/task' import { EmptyMarkup } from '@hcengineering/text' import { StyledTextArea } from '@hcengineering/text-editor-resources' @@ -40,6 +40,8 @@ import { createEventDispatcher } from 'svelte' import recruit from '../../plugin' import IconCompany from '../icons/Company.svelte' + import { Analytics } from '@hcengineering/analytics' + import { getCandidateIdentifier } from '../../utils' // export let space: Ref export let candidate: Ref @@ -117,22 +119,31 @@ ) } - await client.addCollection(recruit.class.Review, doc.space, doc.attachedTo, doc.attachedToClass, 'reviews', { - number: (incResult as any).object.sequence, - date: startDate ?? 0, - dueDate: dueDate ?? 0, - description, - verdict: '', - title, - participants: doc.participants, - company, - application, - location, - access: 'reader', - allDay: false, - eventId: '', - calendar: undefined - }) + const ref = await client.addCollection( + recruit.class.Review, + doc.space, + doc.attachedTo, + doc.attachedToClass, + 'reviews', + { + number: (incResult as any).object.sequence, + date: startDate ?? 0, + dueDate: dueDate ?? 0, + description, + verdict: '', + title, + participants: doc.participants, + company, + application, + location, + access: 'reader', + allDay: false, + eventId: '', + calendar: undefined + } + ) + + Analytics.handleEvent(RecruitEvents.ReviewCreated, { id: ref }) } async function invokeValidate ( diff --git a/plugins/recruit-resources/src/components/review/EditReview.svelte b/plugins/recruit-resources/src/components/review/EditReview.svelte index f80b6259f73..16bec8be716 100644 --- a/plugins/recruit-resources/src/components/review/EditReview.svelte +++ b/plugins/recruit-resources/src/components/review/EditReview.svelte @@ -17,12 +17,13 @@ import contact, { Contact } from '@hcengineering/contact' import { UserBox } from '@hcengineering/contact-resources' import { getClient } from '@hcengineering/presentation' - import type { Review } from '@hcengineering/recruit' + import { RecruitEvents, Review } from '@hcengineering/recruit' import { FullDescriptionBox } from '@hcengineering/text-editor-resources' import { EditBox, Grid } from '@hcengineering/ui' import { ObjectPresenter, openDoc } from '@hcengineering/view-resources' import { createEventDispatcher, onMount } from 'svelte' import recruit from '../../plugin' + import { Analytics } from '@hcengineering/analytics' export let object: Review @@ -52,6 +53,10 @@ } $: updateSelected(object) + + onMount(() => { + Analytics.handleEvent(RecruitEvents.ReviewViewed, { id: object._id }) + }) {#if object !== undefined} diff --git a/plugins/recruit-resources/src/utils.ts b/plugins/recruit-resources/src/utils.ts index 0f2bdd719d6..e22880226c7 100644 --- a/plugins/recruit-resources/src/utils.ts +++ b/plugins/recruit-resources/src/utils.ts @@ -21,7 +21,7 @@ type RecruitDocument = Vacancy | Applicant | Review export async function objectLinkProvider (doc: RecruitDocument): Promise { const location = getCurrentResolvedLocation() const frontUrl = getMetadata(presentation.metadata.FrontUrl) ?? window.location.origin - const url = `${frontUrl}/${workbenchId}/${location.path[1]}/${recruitId}/${await getSequenceId(doc)}` + const url = `${frontUrl}/${workbenchId}/${location.path[1]}/${recruitId}/${getSequenceId(doc)}` return url } @@ -188,7 +188,7 @@ export async function getSequenceLink (doc: RecruitDocument): Promise loc.fragment = undefined loc.query = undefined loc.path[2] = recruitId - loc.path[3] = await getSequenceId(doc) + loc.path[3] = getSequenceId(doc) return loc } @@ -220,6 +220,12 @@ export async function getAppTitle (client: Client, ref: Ref, doc?: Ap return getName(client.getHierarchy(), candidate) } +export function getCandidateIdentifier (ref: Ref): string { + const hierarchy = getClient().getHierarchy() + const clazz = hierarchy.getClass(recruit.mixin.Candidate) + return clazz.shortLabel !== undefined ? `${clazz.shortLabel}-${ref}` : ref +} + export async function getAppIdentifier (client: Client, ref: Ref, doc?: Applicant): Promise { const applicant = doc ?? (await client.findOne(recruit.class.Applicant, { _id: ref })) @@ -235,7 +241,7 @@ export async function getRevTitle (client: Client, ref: Ref, doc?: Revie return object != null ? object.title : '' } -export async function getSequenceId (doc: RecruitDocument): Promise { +export function getSequenceId (doc: RecruitDocument): string { const client = getClient() const hierarchy = client.getHierarchy() if (hierarchy.isDerived(doc._class, recruit.class.Applicant)) { @@ -262,7 +268,7 @@ export async function getVacancyIdentifier (client: Client, ref: Ref, d return '' } - return await getSequenceId(vacancy) + return getSequenceId(vacancy) } export async function getReviewIdentifier (client: Client, ref: Ref, doc?: Review): Promise { @@ -272,5 +278,5 @@ export async function getReviewIdentifier (client: Client, ref: Ref, doc return '' } - return await getSequenceId(review) + return getSequenceId(review) } diff --git a/plugins/recruit/src/analytics.ts b/plugins/recruit/src/analytics.ts new file mode 100644 index 00000000000..36ca4414eb1 --- /dev/null +++ b/plugins/recruit/src/analytics.ts @@ -0,0 +1,11 @@ +export enum RecruitEvents { + NewTalentButtonClicked = 'recruit.talent.NewButtonClicked', + PlusTalentButtonClicked = 'recruit.talent.PlusButtonClicked', + TalentCreated = 'recruit.talent.Created', + ReviewCreated = 'recruit.review.Created', + ReviewViewed = 'recruit.review.Viewed', + VacancyCreated = 'recruit.vacancy.Created', + CompanyCreated = 'recruit.company.Created', + ApplicationCreated = 'recruit.application.Created', + SkillCreated = 'recruit.skill.Created' +} diff --git a/plugins/recruit/src/index.ts b/plugins/recruit/src/index.ts index c9a8cce4fec..ca2f008e068 100644 --- a/plugins/recruit/src/index.ts +++ b/plugins/recruit/src/index.ts @@ -21,6 +21,7 @@ import { AnyComponent, Location, ResolvedLocation } from '@hcengineering/ui' import type { Applicant, ApplicantMatch, Candidate, Opinion, Review, Vacancy, VacancyList } from './types' export * from './types' +export * from './analytics' /** * @public diff --git a/plugins/setting-resources/src/components/Settings.svelte b/plugins/setting-resources/src/components/Settings.svelte index 77bc4123b8d..59c3b52436d 100644 --- a/plugins/setting-resources/src/components/Settings.svelte +++ b/plugins/setting-resources/src/components/Settings.svelte @@ -18,7 +18,7 @@ import login, { loginId } from '@hcengineering/login' import { setMetadata } from '@hcengineering/platform' import presentation, { closeClient, createQuery } from '@hcengineering/presentation' - import setting, { SettingsCategory } from '@hcengineering/setting' + import setting, { SettingsCategory, SettingsEvents } from '@hcengineering/setting' import { Component, Label, @@ -40,6 +40,7 @@ import { NavFooter } from '@hcengineering/workbench-resources' import { ComponentType, onDestroy } from 'svelte' import { clearSettingsStore, settingsStore, type SettingsStore } from '../store' + import { Analytics } from '@hcengineering/analytics' let category: SettingsCategory | undefined let categoryId: string = '' @@ -99,12 +100,15 @@ setMetadataLocalStorage(login.metadata.LoginEndpoint, null) setMetadataLocalStorage(login.metadata.LoginEmail, null) void closeClient() + Analytics.handleEvent(SettingsEvents.SignOut) navigate({ path: [loginId] }) } function selectWorkspace (): void { + Analytics.handleEvent(SettingsEvents.SelectWorkspace) navigate({ path: [loginId, 'selectWorkspace'] }) } function inviteWorkspace (): void { + Analytics.handleEvent(SettingsEvents.InviteToWorkspace) showPopup(login.component.InviteLink, {}) } diff --git a/plugins/setting/src/analytics.ts b/plugins/setting/src/analytics.ts new file mode 100644 index 00000000000..99c64856552 --- /dev/null +++ b/plugins/setting/src/analytics.ts @@ -0,0 +1,5 @@ +export enum SettingsEvents { + SelectWorkspace = 'settings.SelectWorkspace', + InviteToWorkspace = 'settings.InviteToWorkspace', + SignOut = 'settings.SignOut' +} diff --git a/plugins/setting/src/index.ts b/plugins/setting/src/index.ts index 77369a42d40..13237257fbb 100644 --- a/plugins/setting/src/index.ts +++ b/plugins/setting/src/index.ts @@ -23,6 +23,7 @@ import { SpaceTypeCreator, SpaceTypeEditor } from './spaceTypeEditor' export * from './spaceTypeEditor' export * from './utils' +export * from './analytics' /** * @public diff --git a/plugins/tags-resources/package.json b/plugins/tags-resources/package.json index 41a10604173..8709c309aed 100644 --- a/plugins/tags-resources/package.json +++ b/plugins/tags-resources/package.json @@ -38,6 +38,7 @@ "svelte-eslint-parser": "^0.33.1" }, "dependencies": { + "@hcengineering/analytics": "^0.6.0", "@hcengineering/platform": "^0.6.11", "svelte": "^4.2.12", "@hcengineering/tags": "^0.6.16", diff --git a/plugins/tags-resources/src/components/CreateTagElement.svelte b/plugins/tags-resources/src/components/CreateTagElement.svelte index 86c0c7363fe..99faee4053b 100644 --- a/plugins/tags-resources/src/components/CreateTagElement.svelte +++ b/plugins/tags-resources/src/components/CreateTagElement.svelte @@ -77,7 +77,7 @@ }) async function createTagElementFnc (): Promise { - const res = await createTagElement(title, targetClass, category, description, color) + const res = await createTagElement(title, targetClass, category, description, color, keyTitle) dispatch('close', res) } diff --git a/plugins/tags-resources/src/components/TagsAttributeEditor.svelte b/plugins/tags-resources/src/components/TagsAttributeEditor.svelte index 00691fe6152..951d314e619 100644 --- a/plugins/tags-resources/src/components/TagsAttributeEditor.svelte +++ b/plugins/tags-resources/src/components/TagsAttributeEditor.svelte @@ -2,9 +2,11 @@ import { AnyAttribute, Class, Doc, Ref } from '@hcengineering/core' import { IntlString } from '@hcengineering/platform' import { createQuery, getClient } from '@hcengineering/presentation' - import type { TagReference } from '@hcengineering/tags' - import tags from '@hcengineering/tags' + import tags, { TagReference, TagsEvents } from '@hcengineering/tags' import { Icon, Label, getEventPopupPositionElement, showPopup } from '@hcengineering/ui' + import { getObjectId } from '@hcengineering/view-resources' + import { Analytics } from '@hcengineering/analytics' + import TagReferencePresenter from './TagReferencePresenter.svelte' import TagsEditorPopup from './TagsEditorPopup.svelte' import TagIcon from './icons/TagIcon.svelte' @@ -18,6 +20,7 @@ let items: TagReference[] = [] const query = createQuery() const client = getClient() + const hierarchy = client.getHierarchy() $: query.query(tags.class.TagReference, { attachedTo: object._id }, (result) => { items = result @@ -31,7 +34,11 @@ }) } async function removeTag (tag: TagReference): Promise { - if (tag !== undefined) await client.remove(tag) + if (tag !== undefined) { + await client.remove(tag) + const id = await getObjectId(object, hierarchy) + Analytics.handleEvent(TagsEvents.TagRemoved, { object: id }) + } } diff --git a/plugins/tags-resources/src/components/TagsEditorPopup.svelte b/plugins/tags-resources/src/components/TagsEditorPopup.svelte index 69ae2c06ce0..9be7ce487d1 100644 --- a/plugins/tags-resources/src/components/TagsEditorPopup.svelte +++ b/plugins/tags-resources/src/components/TagsEditorPopup.svelte @@ -15,8 +15,11 @@ diff --git a/plugins/tracker-resources/src/components/issues/StatusEditor.svelte b/plugins/tracker-resources/src/components/issues/StatusEditor.svelte index d3ae09c78de..83388bd47be 100644 --- a/plugins/tracker-resources/src/components/issues/StatusEditor.svelte +++ b/plugins/tracker-resources/src/components/issues/StatusEditor.svelte @@ -17,7 +17,7 @@ import { getClient } from '@hcengineering/presentation' import { getTaskTypeStates } from '@hcengineering/task' import { taskTypeStore } from '@hcengineering/task-resources' - import { Issue, IssueDraft, IssueStatus, Project } from '@hcengineering/tracker' + import { Issue, IssueDraft, IssueStatus, Project, TrackerEvents } from '@hcengineering/tracker' import { Button, ButtonKind, @@ -29,10 +29,13 @@ showPopup } from '@hcengineering/ui' import { statusStore } from '@hcengineering/view-resources' + import { Analytics } from '@hcengineering/analytics' import { createEventDispatcher } from 'svelte' + import tracker from '../../plugin' import IssueStatusIcon from './IssueStatusIcon.svelte' import StatusPresenter from './StatusPresenter.svelte' + type ValueType = Issue | (AttachedData & { space: Ref }) | IssueDraft export let value: ValueType @@ -67,6 +70,10 @@ if ('_class' in value) { await client.update(value, { status: newStatus }) + Analytics.handleEvent(TrackerEvents.IssueSetStatus, { + issue: value.identifier, + status: newStatus + }) } } diff --git a/plugins/tracker-resources/src/components/issues/edit/ControlPanel.svelte b/plugins/tracker-resources/src/components/issues/edit/ControlPanel.svelte index 984f8d48169..bbb1e7aa2ed 100644 --- a/plugins/tracker-resources/src/components/issues/edit/ControlPanel.svelte +++ b/plugins/tracker-resources/src/components/issues/edit/ControlPanel.svelte @@ -209,7 +209,15 @@ {#if keys.length > 0}
{#each keys as key (typeof key === 'string' ? key : key.key)} - + {/each} {/if} @@ -220,6 +228,7 @@ {#each mixinKeys as key (typeof key === 'string' ? key : key.key)} | string export let _class: Ref> @@ -128,6 +130,7 @@ if (trimmedTitle.length > 0 && trimmedTitle !== issue.title?.trim()) { await client.update(issue, { title: trimmedTitle }) + Analytics.handleEvent(TrackerEvents.IssueTitleUpdated, { issue: issue.identifier ?? issue._id }) } } @@ -284,6 +287,7 @@ {readonly} key={{ key: 'description', attr: descriptionKey }} bind:this={descriptionBox} + identifier={issue?.identifier} placeholder={tracker.string.IssueDescriptionPlaceholder} boundary={content} on:saved={(evt) => { diff --git a/plugins/tracker-resources/src/components/issues/timereport/TimeSpendReportPopup.svelte b/plugins/tracker-resources/src/components/issues/timereport/TimeSpendReportPopup.svelte index 891c8a00cf4..37dd5b19f5c 100644 --- a/plugins/tracker-resources/src/components/issues/timereport/TimeSpendReportPopup.svelte +++ b/plugins/tracker-resources/src/components/issues/timereport/TimeSpendReportPopup.svelte @@ -18,12 +18,13 @@ import type { IntlString } from '@hcengineering/platform' import presentation, { Card, getClient } from '@hcengineering/presentation' import { UserBox } from '@hcengineering/contact-resources' - import { Issue, TimeReportDayType, TimeSpendReport } from '@hcengineering/tracker' + import { Issue, TimeReportDayType, TimeSpendReport, TrackerEvents } from '@hcengineering/tracker' import { Button, DatePresenter, EditBox, Label } from '@hcengineering/ui' import tracker from '../../../plugin' import { getTimeReportDate, getTimeReportDayType } from '../../../utils' import TitlePresenter from '../TitlePresenter.svelte' import TimeReportDayDropdown from './TimeReportDayDropdown.svelte' + import { Analytics } from '@hcengineering/analytics' export let issue: Issue | undefined = undefined export let issueId: Ref | undefined = issue?._id @@ -61,6 +62,7 @@ 'reports', data as AttachedData ) + Analytics.handleEvent(TrackerEvents.IssueTimeSpentAdded, { issue: issue?.identifier ?? issueId }) } } else { const ops: DocumentUpdate = {} @@ -78,6 +80,7 @@ } if (Object.keys(ops).length > 0) { await client.update(value, ops) + Analytics.handleEvent(TrackerEvents.IssueTimeSpentUpdated, { issue: issue?.identifier ?? issueId }) } } } diff --git a/plugins/tracker-resources/src/components/milestones/MilestoneEditor.svelte b/plugins/tracker-resources/src/components/milestones/MilestoneEditor.svelte index 15dfffddaac..5a190d829cc 100644 --- a/plugins/tracker-resources/src/components/milestones/MilestoneEditor.svelte +++ b/plugins/tracker-resources/src/components/milestones/MilestoneEditor.svelte @@ -16,7 +16,8 @@ import { Ref } from '@hcengineering/core' import { IntlString } from '@hcengineering/platform' import { createQuery, getClient } from '@hcengineering/presentation' - import { Issue, IssueTemplate, Milestone, Project } from '@hcengineering/tracker' + import { Issue, IssueTemplate, Milestone, Project, TrackerEvents } from '@hcengineering/tracker' + import { Analytics } from '@hcengineering/analytics' import { ButtonKind, ButtonShape, @@ -58,10 +59,18 @@ await Promise.all( value.map(async (p) => { await client.update(p, { milestone: newMilestoneId }) + Analytics.handleEvent(TrackerEvents.IssueMilestoneAdded, { + issue: p.identifier ?? p._id, + component: newMilestoneId + }) }) ) } else { await client.update(value, { milestone: newMilestoneId }) + Analytics.handleEvent(TrackerEvents.IssueMilestoneAdded, { + issue: (value as Issue).identifier ?? value._id, + component: newMilestoneId + }) } if (isAction) dispatch('close') } diff --git a/plugins/tracker-resources/src/components/projects/CreateProject.svelte b/plugins/tracker-resources/src/components/projects/CreateProject.svelte index 7ba05e0886b..9390e32d26c 100644 --- a/plugins/tracker-resources/src/components/projects/CreateProject.svelte +++ b/plugins/tracker-resources/src/components/projects/CreateProject.svelte @@ -31,7 +31,7 @@ import presentation, { Card, createQuery, getClient } from '@hcengineering/presentation' import task, { ProjectType, TaskType } from '@hcengineering/task' import { taskTypeStore, typeStore } from '@hcengineering/task-resources' - import { IssueStatus, Project, TimeReportDayType } from '@hcengineering/tracker' + import { IssueStatus, Project, TimeReportDayType, TrackerEvents } from '@hcengineering/tracker' import { Button, Component, @@ -49,6 +49,8 @@ import { IconPicker } from '@hcengineering/view-resources' import { deepEqual } from 'fast-equals' import { createEventDispatcher } from 'svelte' + import { Analytics } from '@hcengineering/analytics' + import tracker from '../../plugin' import StatusSelector from '../issues/StatusSelector.svelte' @@ -234,7 +236,10 @@ isSaving = true await ops.createDoc(tracker.class.Project, core.space.Space, { ...projectData, type: typeId }, projectId) const succeeded = await ops.commit() - + Analytics.handleEvent(TrackerEvents.ProjectCreated, { + ok: succeeded.result, + id: projectData.identifier + }) if (succeeded.result) { // Add space type's mixin with roles assignments await client.createMixin( diff --git a/plugins/tracker-resources/src/components/templates/EstimationEditor.svelte b/plugins/tracker-resources/src/components/templates/EstimationEditor.svelte index 04e64ad6825..e4c3d8db558 100644 --- a/plugins/tracker-resources/src/components/templates/EstimationEditor.svelte +++ b/plugins/tracker-resources/src/components/templates/EstimationEditor.svelte @@ -15,12 +15,13 @@