Use our analytics service properly (#9985)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-10-01 12:30:23 +07:00
committed by GitHub
parent 7d706ec4f1
commit 2b39a954a0
79 changed files with 328 additions and 429 deletions
-3
View File
@@ -8,9 +8,6 @@ export interface Config {
ACCOUNTS_URL: string
AI_URL?: string
ANALYTICS_COLLECTOR_URL?: string
POSTHOG_API_KEY?: string
POSTHOG_HOST?: string
SENTRY_DSN?: string
BRANDING_URL?: string
CALENDAR_URL: string
COLLABORATOR?: string
+17 -16
View File
@@ -426,22 +426,23 @@ services:
# - STATS_URL=http://huly.local:4900
# # - LOVE_ENDPOINT=http://huly.local:8096
# # - OPENAI_API_KEY=token
# analytics:
# image: hardcoreeng/analytics-collector
# extra_hosts:
# - 'huly.local:host-gateway'
# restart: unless-stopped
# ports:
# - 4017:4017
# environment:
# - SECRET=secret
# - PORT=4017
# - SERVICE_ID=analytics-collector-service
# - ACCOUNTS_URL=http://huly.local:3000
# - STATS_URL=http://huly.local:4900
# - POSTHOG_HOST=${POSTHOG_HOST}
# - POSTHOG_API_KEY=${POSTHOG_API_KEY}
# - MAX_PAYLOAD_SIZE=10mb
analytics:
image: hardcoreeng/analytics-collector
extra_hosts:
- 'huly.local:host-gateway'
restart: unless-stopped
ports:
- 4017:4017
environment:
- SECRET=secret
- PORT=4017
- SERVICE_ID=analytics-collector-service
- ACCOUNTS_URL=http://huly.local:3000
- STATS_URL=http://huly.local:4900
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
# - POSTHOG_HOST=${POSTHOG_HOST}
# - POSTHOG_API_KEY=${POSTHOG_API_KEY}
# - MAX_PAYLOAD_SIZE=10mb
export:
image: hardcoreeng/export
extra_hosts:
-2
View File
@@ -282,8 +282,6 @@
"@hcengineering/ai-assistant": "^0.6.0",
"@hcengineering/ai-assistant-assets": "^0.6.0",
"@hcengineering/ai-assistant-resources": "^0.6.0",
"@sentry/svelte": "^9.22.0",
"posthog-js": "^1.246.0",
"readable-stream": "^4.7.0",
"svelte": "^4.2.20"
}
+2 -1
View File
@@ -29,5 +29,6 @@
"BACKUP_URL": "http://huly.local:4039/api/backup",
"PULSE_URL": "ws://huly.local:8099/ws",
"HULYLAKE_URL": "http://huly.local:8096",
"EXCLUDED_APPLICATIONS_FOR_ANONYMOUS": "[\"chunter\", \"notification\"]"
"EXCLUDED_APPLICATIONS_FOR_ANONYMOUS": "[\"chunter\", \"notification\"]",
"ANALYTICS_COLLECTOR_URL": "http://huly.local:4017"
}
-3
View File
@@ -168,13 +168,10 @@ export interface Config {
GITHUB_APP?: string
GITHUB_CLIENTID?: string
GITHUB_URL: string
SENTRY_DSN?: string
LOVE_ENDPOINT?: string
LIVEKIT_WS?: string
SIGN_URL?: string
PRINT_URL?: string
POSTHOG_API_KEY?: string
POSTHOG_HOST?: string
ANALYTICS_COLLECTOR_URL?: string
BRANDING_URL?: string
TELEGRAM_BOT_URL?: string
@@ -45,8 +45,6 @@
"@hcengineering/analytics-collector": "^0.6.0",
"@hcengineering/presentation": "^0.6.3",
"@hcengineering/core": "^0.6.32",
"posthog-js": "^1.246.0",
"@sentry/svelte": "^9.22.0",
"ua-parser-js": "^2.0.4",
"@hcengineering/ui": "^0.6.15"
},
@@ -14,11 +14,11 @@
//
import { type AnalyticProvider } from '@hcengineering/analytics'
import presentation from '@hcengineering/presentation'
import { getMetadata } from '@hcengineering/platform'
import { AnalyticEventType } from '@hcengineering/analytics-collector'
import { collectEventMetadata, triggerUrlChange } from './utils'
import { getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { type QueuedEvent } from './types'
import { collectEventMetadata, triggerUrlChange } from './utils'
export class AnalyticsCollectorProvider implements AnalyticProvider {
private readonly collectIntervalMs = 5000
@@ -39,6 +39,7 @@ export class AnalyticsCollectorProvider implements AnalyticProvider {
if (this.url !== undefined && this.url !== '' && this.url !== null) {
this.initializeAnonymousId()
this.startCollectionTimer()
this.registerExceptionHandlers()
return true
}
return false
@@ -48,6 +49,18 @@ export class AnalyticsCollectorProvider implements AnalyticProvider {
this.anonymousId = this.generateAnonymousId()
}
private registerExceptionHandlers (): void {
// Capture unhandled errors
window.addEventListener('error', (event) => {
this.handleError(event.error ?? new Error(event.message))
})
// Capture unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
this.handleError(event.reason instanceof Error ? event.reason : new Error(String(event.reason)))
})
}
private generateAnonymousId (): string {
return 'anon_' + Date.now() + '_' + Math.random().toString(36).substring(2, 15)
}
@@ -151,7 +164,13 @@ export class AnalyticsCollectorProvider implements AnalyticProvider {
$is_identified: this.isAuthenticated
}
const eventMetadata: Record<string, any> = collectEventMetadata(baseProperties)
let eventMetadata: Record<string, any> = {}
try {
eventMetadata = collectEventMetadata(baseProperties)
} catch (err: any) {
// Ignore metadata collection errors
}
if (eventType === AnalyticEventType.CustomEvent && (eventName !== '' || eventName != null)) {
eventMetadata.event = eventName
}
@@ -271,16 +290,20 @@ export class AnalyticsCollectorProvider implements AnalyticProvider {
handleError (error: Error): void {
const currentId = this.isAuthenticated && (this.email != null || this.email !== '') ? this.email : this.anonymousId
this.addEvent(
AnalyticEventType.Error,
{
const event: QueuedEvent = {
event: AnalyticEventType.Error,
properties: {
error_message: error.message ?? 'Unknown error',
error_type: error.name ?? 'Error',
error_stack: error.stack ?? ''
},
'$exception',
currentId
)
timestamp: Date.now(),
distinct_id: currentId ?? ''
}
this.events.push(event)
// We need to trigger sending immediately for errors
void this.sendEvents()
}
navigate (path: string): void {
@@ -15,22 +15,14 @@
import { type AnalyticProvider, Analytics } from '@hcengineering/analytics'
import { AnalyticsCollectorProvider } from './analyticsCollector'
import { PosthogAnalyticProvider } from './posthog'
import { SentryAnalyticProvider } from './sentry'
import { type AnalyticsConfig } from './types'
export * from './analyticsCollector'
export * from './posthog'
export * from './sentry'
export * from './utils'
export * from './types'
export function configureAnalyticsProviders (config: AnalyticsConfig): void {
const providers: AnalyticProvider[] = [
new AnalyticsCollectorProvider(),
new SentryAnalyticProvider(),
new PosthogAnalyticProvider()
]
const providers: AnalyticProvider[] = [new AnalyticsCollectorProvider()]
for (const provider of providers) {
Analytics.init(provider, config)
@@ -14,8 +14,6 @@
//
export * from './analyticsCollector'
export * from './posthog'
export * from './sentry'
export * from './utils'
export * from './types'
export * from './configure'
@@ -1,68 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { type AnalyticProvider } from '@hcengineering/analytics'
import posthog from 'posthog-js'
export class PosthogAnalyticProvider implements AnalyticProvider {
init (config: Record<string, any>): boolean {
if (config.POSTHOG_API_KEY !== undefined && config.POSTHOG_API_KEY !== '' && config.POSTHOG_HOST !== null) {
posthog.init(config.POSTHOG_API_KEY, {
api_host: config.POSTHOG_HOST,
autocapture: false,
capture_pageview: false,
capture_pageleave: false
})
return true
}
return false
}
setUser (email: string): void {
if (!posthog._isIdentified()) {
posthog.identify(email, { email })
}
}
setAlias (distinctId: string, alias: string): void {
posthog.alias(alias, distinctId)
}
setTag (key: string, value: string | number): void {
posthog.setPersonProperties({ [key]: value })
}
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 {
posthog.reset(true)
}
handleEvent (event: string, params: Record<string, any>): void {
posthog.capture(event, params)
}
handleError (error: Error): void {
posthog.capture(error.message)
}
navigate (path: string): void {
posthog.capture('$pageview')
}
}
@@ -1,75 +0,0 @@
//
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { type AnalyticProvider } from '@hcengineering/analytics'
import * as Sentry from '@sentry/svelte'
export class SentryAnalyticProvider implements AnalyticProvider {
navigate (path: string): void {}
init (config: Record<string, any>): boolean {
if (config.SENTRY_DSN !== undefined && config.SENTRY_DSN !== '') {
Sentry.init({
dsn: config.SENTRY_DSN,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
maskAllText: false,
blockAllMedia: false
})
],
// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 1.0,
tracePropagationTargets: [/^https:\/\/huly\.app/, /^https:\/\/app\.huly\.io/, /^https:\/\/account\.huly\.io/],
replaysSessionSampleRate: 0.0,
replaysOnErrorSampleRate: 1.0
})
return true
}
return false
}
setUser (email: string): void {
Sentry.setUser({ email })
}
setAlias (distinctId: string, alias: string): void {}
logout (): void {
Sentry.setUser(null)
}
setTag (key: string, value: string | number): void {
Sentry.setTag(key, value)
}
setWorkspace (ws: string, guest: boolean): void {
const prop: string = guest ? 'visited-workspace' : 'workspace'
this.setTag(prop, ws)
}
handleEvent (event: string): void {
// currently we don't need it, but maybe in future
// Sentry.captureMessage(event, 'log')
}
handleError (error: Error): void {
Sentry.captureException(error)
}
}
@@ -17,7 +17,6 @@ import { type AnalyticEvent } from '@hcengineering/analytics-collector'
export interface AnalyticsConfig {
ANALYTICS_COLLECTOR_URL?: string
SENTRY_DSN?: string
POSTHOG_API?: string
POSTHOG_HOST?: string
[key: string]: any
@@ -37,6 +37,7 @@
import presentation from '..'
import { ObjectCreate } from '../types'
import { getClient } from '../utils'
import { Analytics } from '@hcengineering/analytics'
export let _class: Ref<Class<Doc>>
export let objects: Doc[] = []
@@ -184,7 +185,7 @@
presenter = result
})
.catch((err) => {
console.error('Failed to find presenter for class ' + _class, err)
Analytics.handleError(err)
})
}
}
@@ -136,7 +136,6 @@
} catch (error: any) {
commandProcessor.set([])
Analytics.handleError(error)
console.error('Failed to parse drawing content', error)
}
} else {
commandProcessor.set([])
@@ -151,7 +150,6 @@
}
createDrawing(data).catch((error) => {
Analytics.handleError(error)
console.error('Failed to save drawing', error)
})
}
}
@@ -36,6 +36,7 @@
import DrawingBoardToolbarColorIcon from './DrawingBoardToolbarColorIcon.svelte'
import DrawingBoardColorSelectorIcon from './DrawingBoardColorSelectorIcon.svelte'
import { ColorsList, DrawingBoardColoringSetup } from '../drawingColors'
import { Analytics } from '@hcengineering/analytics'
interface DrawingBoardToolbarEvents {
undo: undefined
@@ -130,9 +131,7 @@
break
}
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustive: never = id
console.error('Unknown command id', id)
Analytics.handleError(new Error(`Unknown command id '${id as any}'`))
}
}
})
@@ -73,7 +73,6 @@
.catch((error) => {
drawingLoading = false
Analytics.handleError(error)
console.error('Failed to load drawings for file', file, error)
})
}
}
@@ -18,6 +18,7 @@
import uiPlugin, { navigate, parseLocation } from '@hcengineering/ui'
import presentation from '../../plugin'
import { Analytics } from '@hcengineering/analytics'
export let mark: MarkupMark
@@ -40,8 +41,8 @@
}
}
}
} catch (err) {
console.error('Failed to handle link', mark, err)
} catch (err: any) {
Analytics.handleError(err)
}
}
</script>
+1
View File
@@ -37,6 +37,7 @@
"typescript": "^5.8.3"
},
"dependencies": {
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/card": "^0.6.0",
"@hcengineering/contact": "^0.6.24",
@@ -22,15 +22,16 @@
} from '@hcengineering/activity'
import { Class, Doc, getCurrentAccount, Ref, SortingOrder } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Grid, Section, Spinner, location, Lazy } from '@hcengineering/ui'
import { Grid, Lazy, location, Section, Spinner } from '@hcengineering/ui'
import { onDestroy, onMount } from 'svelte'
import { editingMessageStore, messageInFocus } from '../activity'
import { combineActivityMessages, sortActivityMessages } from '../activityMessagesUtils'
import { canGroupMessages, getActivityNewestFirst, getMessageFromLoc, getSpace } from '../utils'
import ActivityMessagePresenter from './activity-message/ActivityMessagePresenter.svelte'
import ActivityExtensionComponent from './ActivityExtension.svelte'
import ActivityFilter from './ActivityFilter.svelte'
import { combineActivityMessages, sortActivityMessages } from '../activityMessagesUtils'
import { canGroupMessages, getMessageFromLoc, getSpace, getActivityNewestFirst } from '../utils'
import ActivityMessagePresenter from './activity-message/ActivityMessagePresenter.svelte'
import { editingMessageStore, messageInFocus } from '../activity'
import { Analytics } from '@hcengineering/analytics'
export let object: WithReferences<Doc>
export let showCommenInput: boolean = true
@@ -185,8 +186,8 @@
}
clazz = hierarchy.getClass(clazz).extends
}
} catch (e) {
console.error(e)
} catch (e: any) {
Analytics.handleError(e)
return []
}
return []
@@ -25,6 +25,7 @@
import aiAssistant from '../plugin'
import { buildSocialIdString, getCurrentAccount, SocialIdType, type PersonId } from '@hcengineering/core'
import HulyAssistant from './icons/HulyAssistant.svelte'
import { Analytics } from '@hcengineering/analytics'
export let integration: Integration | undefined = undefined
@@ -51,9 +52,10 @@
isLoading = false
dispatch('close')
} catch (err) {
} catch (err: any) {
isLoading = false
console.error('Failed to find/create huly assistant social id/integration:', err)
Analytics.handleError(err)
}
})
@@ -27,6 +27,7 @@
import { getImageDimensions } from '../utils'
import LinkPreviewIcon from './LinkPreviewIcon.svelte'
import LinkPreviewImage from './LinkPreviewImage.svelte'
import { Analytics } from '@hcengineering/analytics'
export let attachment: WithLookup<Attachment>
export let isOwn = false
@@ -56,7 +57,7 @@
})
.catch((err) => {
viewModel = undefined
console.error(err)
Analytics.handleError(err)
})
})
@@ -18,7 +18,7 @@
try {
attr = getClient().getHierarchy().getAttribute(value.ofClass, value.attributeName)
} catch (err: any) {
console.error(err)
Analytics.handleError(err)
}
</script>
+1
View File
@@ -38,6 +38,7 @@
"svelte-eslint-parser": "^0.33.1"
},
"dependencies": {
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/account-client": "^0.6.0",
"@hcengineering/login": "^0.6.12",
"@hcengineering/core": "^0.6.32",
@@ -13,6 +13,7 @@
import { getMetadata } from '@hcengineering/platform'
import { getCurrentAccount, pickPrimarySocialId, SocialId, SocialIdType } from '@hcengineering/core'
import { getAccountClient } from '../utils'
import { Analytics } from '@hcengineering/analytics'
const workspaceUuid = getCurrentWorkspaceUuid()
@@ -89,7 +90,7 @@
selectedSocialId = socialId
accessEnabled = integrationWs != null && integrationGlobal != null
} catch (err: any) {
console.error('Failed to load CalDAV intergrations', err)
Analytics.handleError(err)
error = `${err.message ?? 'Unable to load CalDAV intergrations'}`
} finally {
loading = false
@@ -151,7 +152,7 @@
dispatch('close', true)
} catch (err: any) {
error = `${err.message ?? 'Unable to save CalDAV intergration'}`
console.error('Failed to save CalDAV integration', err)
Analytics.handleError(err)
} finally {
loading = false
}
@@ -55,6 +55,7 @@
import calendar from '../plugin'
import CalendarSelector from './CalendarSelector.svelte'
import TimeZoneSelector from './TimeZoneSelector.svelte'
import { Analytics } from '@hcengineering/analytics'
export let schedule: Schedule | undefined
@@ -99,8 +100,8 @@
.then((text) => {
durationVariants.push({ msec, text })
})
.catch((err) => {
console.error(err)
.catch((err: any) => {
Analytics.handleError(err)
})
})
}
@@ -112,8 +113,8 @@
.then((text) => {
intervalVariants.push({ msec, text })
})
.catch((err) => {
console.error(err)
.catch((err: any) => {
Analytics.handleError(err)
})
})
}
@@ -170,8 +171,8 @@
.then((res) => {
formattedMeetingDuration = res
})
.catch((err) => {
console.error(err)
.catch((err: any) => {
Analytics.handleError(err)
})
}
@@ -180,8 +181,8 @@
.then((res) => {
formattedMeetingInterval = res
})
.catch((err) => {
console.error(err)
.catch((err: any) => {
Analytics.handleError(err)
})
}
@@ -33,6 +33,7 @@
import chat from '@hcengineering/chat'
import EditorActions from './EditorActions.svelte'
import { Analytics } from '@hcengineering/analytics'
const dispatch = createEventDispatcher()
const communicationClient = getCommunicationClient()
@@ -90,7 +91,7 @@
const createdCard = await createCard(type, space, data, cardDescription, _id)
if (isThread) {
if (createdCard == null) {
console.error('Failed to create thread card')
Analytics.handleError(new Error('Failed to create thread card'))
return
}
const blobs: (BlobParams & { mimeType: string })[] = descriptionBox.getAttachments().map((attachment) => ({
@@ -13,26 +13,27 @@
// limitations under the License.
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { MasterTag } from '@hcengineering/card'
import core, { Doc, Ref } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import {
AnyComponent,
AnySvelteComponent,
BreadcrumbItem,
Breadcrumbs,
Header,
Location,
resizeObserver,
deviceOptionsStore as deviceInfo,
getCurrentLocation,
Header,
Location,
navigate,
resizeObserver,
resolvedLocationStore
} from '@hcengineering/ui'
import { onDestroy, onMount } from 'svelte'
import card from '../../plugin'
import MasterTagEditor from './MasterTagEditor.svelte'
import { getResource } from '@hcengineering/platform'
let masterTag: MasterTag | undefined
let visibleSecondNav: boolean = true
@@ -62,10 +63,10 @@
selectedSubObjectId = objId
void getResource(editorId)
.then((res) => (subEditor = res))
.catch((err) => {
.catch((err: any) => {
subEditor = undefined
subEditorParamas = []
console.error(err)
Analytics.handleError(err)
})
} else {
subEditorParamas = []
@@ -42,8 +42,8 @@
try {
const _classLabel = hierarchy.getClass(_class)
return _classLabel.label
} catch (err) {
console.error(err)
} catch (err: any) {
Analytics.handleError(err)
return core.string.Class
}
}
@@ -17,6 +17,7 @@
import chat from '../../plugin'
import InboxViewSettings from './InboxViewSettings.svelte'
import { Analytics } from '@hcengineering/analytics'
const communicationClient = getCommunicationClient()
@@ -30,9 +31,9 @@
const contexts = await communicationClient.findNotificationContexts({ order: SortingOrder.Ascending })
await Promise.all(contexts.map((context) => communicationClient.removeNotificationContext(context.id)))
clearing = false
} catch (e) {
} catch (e: any) {
clearing = false
console.error(e)
Analytics.handleError(e)
}
}
@@ -23,6 +23,7 @@
import { getChannelName, getObjectIcon } from '../utils'
import chunter from '../plugin'
import { Analytics } from '@hcengineering/analytics'
export let object: Doc
export let readonly = false
@@ -48,8 +49,8 @@
}
clazz = hierarchy.getClass(clazz).extends
}
} catch (e) {
console.error(e)
} catch (e: any) {
Analytics.handleError(e)
return []
}
return []
@@ -138,7 +138,6 @@
const objectId = await getObjectId(object, client.getHierarchy())
Analytics.handleEvent(ChunterEvents.MessageCreated, { ok: false, objectId, objectClass: object._class })
Analytics.handleError(err)
console.error(err)
}
}
@@ -151,7 +150,6 @@
const objectId = await getObjectId(object, client.getHierarchy())
Analytics.handleEvent(ChunterEvents.MessageEdited, { ok: false, objectId, objectClass: object._class })
Analytics.handleError(err)
console.error(err)
}
}
@@ -42,6 +42,7 @@
"svelte-eslint-parser": "^0.33.1"
},
"dependencies": {
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/ai-bot": "^0.6.0",
"@hcengineering/ai-bot-resources": "^0.6.0",
"@hcengineering/attachment-resources": "^0.6.0",
@@ -14,51 +14,52 @@
-->
<script lang="ts">
import { Markup, RateLimiter, Ref, generateId } from '@hcengineering/core'
import { tick, createEventDispatcher } from 'svelte'
import {
uploadFile,
deleteFile,
getCommunicationClient,
getClient,
getFileMetadata,
isLinkPreviewEnabled
} from '@hcengineering/presentation'
import {
Message,
MessageID,
LinkPreviewParams,
BlobParams,
AttachmentID,
linkPreviewType,
BlobAttachment,
AppletParams
} from '@hcengineering/communication-types'
import { AttachmentPresenter, LinkPreviewCard } from '@hcengineering/attachment-resources'
import { areEqualMarkups, isEmptyMarkup } from '@hcengineering/text'
import { clearTyping, setTyping } from '@hcengineering/presence-resources'
import { FileUploadCallbackParams, getUploadHandlers, UploadHandlerDefinition } from '@hcengineering/uploader'
import { Component, showPopup, ThrottledCaller } from '@hcengineering/ui'
import { getCurrentEmployee } from '@hcengineering/contact'
import { Card } from '@hcengineering/card'
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { isAppletAttachment, isBlobAttachment, isLinkPreviewAttachment } from '@hcengineering/communication-shared'
import {
AppletParams,
AttachmentID,
BlobAttachment,
BlobParams,
LinkPreviewParams,
linkPreviewType,
Message,
MessageID
} from '@hcengineering/communication-types'
import { getCurrentEmployee } from '@hcengineering/contact'
import { generateId, Markup, RateLimiter, Ref } from '@hcengineering/core'
import { getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import { clearTyping, setTyping } from '@hcengineering/presence-resources'
import {
deleteFile,
getClient,
getCommunicationClient,
getFileMetadata,
isLinkPreviewEnabled,
uploadFile
} from '@hcengineering/presentation'
import { areEqualMarkups, isEmptyMarkup } from '@hcengineering/text'
import { Component, showPopup, ThrottledCaller } from '@hcengineering/ui'
import { FileUploadCallbackParams, getUploadHandlers, UploadHandlerDefinition } from '@hcengineering/uploader'
import { createEventDispatcher, tick } from 'svelte'
import TextInput from '../TextInput.svelte'
import IconAttach from '../icons/Attach.svelte'
import { Analytics } from '@hcengineering/analytics'
import { getDraft, getEmptyDraft, messageToDraft, removeDraft, saveDraft } from '../../draft'
import communication from '../../plugin'
import { messageEditingStore } from '../../stores'
import { type TextInputAction, AppletDraft, MessageDraft } from '../../types'
import {
defaultMessageInputActions,
toMarkdown,
toMarkup,
loadLinkPreviewParams,
isCardAllowedForCommunications,
showForbidden
loadLinkPreviewParams,
showForbidden,
toMarkdown,
toMarkup
} from '../../utils'
import communication from '../../plugin'
import { type TextInputAction, MessageDraft, AppletDraft } from '../../types'
import TextInput from '../TextInput.svelte'
import TypingPresenter from '../TypingPresenter.svelte'
import { getDraft, messageToDraft, saveDraft, getEmptyDraft, removeDraft } from '../../draft'
import { messageEditingStore } from '../../stores'
import IconAttach from '../icons/Attach.svelte'
export let card: Card
export let message: Message | undefined = undefined
@@ -156,8 +157,8 @@
const r = await getResource(ap.createFn)
await r(card, messageId, appletDraft.params)
toAttach.push(appletDraft)
} catch (err) {
console.error(err)
} catch (err: any) {
Analytics.handleError(err)
}
}
@@ -13,6 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { Contact, Employee, getCurrentEmployee, getName, Person } from '@hcengineering/contact'
import { AccountUuid, notEmpty, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
@@ -48,7 +49,7 @@
const empRef = $employeeRefByAccountUuidStore.get(uuid)
if (empRef === undefined) {
console.error('Employee not found by account id', uuid)
Analytics.handleError(new Error(`Employee not found by account id ${uuid}`))
return null
}
@@ -21,6 +21,7 @@
import contact from '../plugin'
import { employeeByPersonIdStore, primarySocialIdByEmployeeRefStore } from '../utils'
import UserBox from './UserBox.svelte'
import { Analytics } from '@hcengineering/analytics'
export let label: IntlString = contact.string.Employee
export let value: PersonId | null | undefined
@@ -73,6 +74,7 @@
}
if (socialString === undefined) {
console.error('Social id not found for person', e.detail)
Analytics.handleError(new Error(`Social id not found for person ${e.detail}`))
return
}
@@ -7,6 +7,7 @@
import contact from '../plugin'
import UsersPopup from './UsersPopup.svelte'
import { employeeByIdStore, employeeRefByAccountUuidStore } from '../utils'
import { Analytics } from '@hcengineering/analytics'
export let value: Space
const dispatch = createEventDispatcher()
@@ -19,6 +20,7 @@
if (employeeRef === undefined) {
console.error(`Employee with account ${acc} not found`)
Analytics.handleError(new Error(`Employee with account ${acc} not found`))
return undefined
}
@@ -22,6 +22,7 @@
import contact from '../plugin'
import { employeeByPersonIdStore, primarySocialIdByEmployeeRefStore } from '../utils'
import UserBoxList from './UserBoxList.svelte'
import { Analytics } from '@hcengineering/analytics'
export let label: IntlString
export let value: PersonId[]
@@ -47,6 +48,7 @@
if (ref == null) {
console.error('Employee not found for social id', p)
Analytics.handleError(new Error(`Employee not found for social id ${p}`))
return null
}
@@ -71,6 +73,7 @@
if (primaryId === undefined) {
console.error('Primary social id not found for person', person)
Analytics.handleError(new Error(`Primary social id not found for person ${person}`))
continue
}
@@ -28,6 +28,7 @@
import { getIntegrationClient } from '../api'
import gmail from '../plugin'
import { isNewGmailIntegration } from '../utils'
import { Analytics } from '@hcengineering/analytics'
export let integration: AccountIntegration
let integrationSettings: Integration | undefined
@@ -51,7 +52,9 @@
async function change (shared: AccountUuid[]) {
if (integrationSettings == null) {
console.error('Integrations settings are not found', integration.socialId, integration.kind)
Analytics.handleError(
new Error(`Integrations settings are not found for ${integration.socialId} ${integration.kind}`)
)
return
}
integrationSettings.shared = shared
@@ -27,6 +27,7 @@
import gmail from '../plugin'
import core, { getCurrentAccount, Ref, Space } from '@hcengineering/core'
import GmailColor from './icons/GmailColor.svelte'
import { Analytics } from '@hcengineering/analytics'
export let integration: Integration
@@ -44,9 +45,10 @@
personSpace = personSpaceObj?._id
selectedSpace = integrationSpace ?? personSpace
isLoading = false
} catch (err) {
} catch (err: any) {
isLoading = false
console.error('Failed to find person space:', err)
Analytics.handleError(err)
}
})
@@ -49,7 +49,6 @@
dispatch('close')
} catch (e: any) {
Analytics.handleError(e)
console.error(e)
error = true
} finally {
connecting = false
@@ -23,6 +23,7 @@
import gmail from '../plugin'
import { getState } from '../api'
import platform, { IntlString, OK, ERROR, Status, Severity } from '@hcengineering/platform'
import { Analytics } from '@hcengineering/analytics'
export let integration: Integration
@@ -40,7 +41,7 @@
isLoading = false
subscribe()
status = state?.status === 'inactive' ? new Status(Severity.WARNING, platform.status.OK, {}) : OK
} catch (err) {
} catch (err: any) {
status = ERROR
error = err instanceof Error ? err.message : 'Failed to load gmail state'
if (error.includes('Failed to fetch')) {
@@ -48,6 +49,7 @@
}
isLoading = false
console.error('Error loading gmail state:', err)
Analytics.handleError(err)
}
})
@@ -75,6 +77,7 @@
state = await getState(integration.socialId)
} catch (err: any) {
console.error('Error refresh gmail state:', err.message)
Analytics.handleError(err)
}
}
@@ -253,7 +253,6 @@
return await titleProvider(client, _id)
} catch (err: any) {
Analytics.handleError(err)
console.error(err)
}
}
@@ -34,6 +34,7 @@
type PersonId
} from '@hcengineering/core'
import HulyMail from './icons/HulyMail.svelte'
import { Analytics } from '@hcengineering/analytics'
export let integration: Integration | undefined = undefined
@@ -57,9 +58,9 @@
personSpace = personSpaceObj?._id
selectedSpace = integrationSpace ?? personSpace
isLoading = false
} catch (err) {
} catch (err: any) {
isLoading = false
console.error('Failed to find person space:', err)
Analytics.handleError(err)
}
})
@@ -72,6 +72,7 @@
} else {
if (result.email == null) {
console.error('No email in auto join info')
Analytics.handleError(new Error('No email in auto join info'))
navigate({ path: [loginId] })
return
}
@@ -82,6 +83,9 @@
}
} catch (err: any) {
console.error('Failed to check auto join', err)
Analytics.handleError(
new Error('Failed to check auto join: ' + (err instanceof Error ? err.message : String(err)))
)
navigate({ path: [loginId] })
return
}
@@ -21,6 +21,7 @@
import { Dropdown, Icon } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { rooms } from '../stores'
import { Analytics } from '@hcengineering/analytics'
export let value: Ref<Room> | undefined
export let disabled: boolean = false
@@ -64,8 +65,8 @@
items = [...items]
}
})
.catch((err) => {
console.error(err)
.catch((err: any) => {
Analytics.handleError(err)
})
} else if (room.person !== null) {
const person = $personByRefStore.get(room.person)
@@ -79,8 +80,8 @@
items = [...items]
}
})
.catch((err) => {
console.error(err)
.catch((err: any) => {
Analytics.handleError(err)
})
}
} else {
@@ -93,8 +94,8 @@
items = [...items]
}
})
.catch((err) => {
console.error(err)
.catch((err: any) => {
Analytics.handleError(err)
})
}
}
@@ -74,7 +74,6 @@
return await parseLinkId(linkProviders, id, _class as Ref<Class<Doc>>)
} catch (err: any) {
Analytics.handleError(err)
console.error(err)
}
}
}
@@ -4,11 +4,12 @@
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { type Blob, type BlobMetadata, type Ref } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import presentation, { getFileUrl } from '@hcengineering/presentation'
import { EmbeddedPDF, Spinner, themeStore } from '@hcengineering/ui'
import { convertToHTML } from '@hcengineering/print'
import { EmbeddedPDF, Spinner, themeStore } from '@hcengineering/ui'
export let value: Ref<Blob>
export let name: string
@@ -29,8 +30,8 @@
convertedFile = res
isLoading = false
},
(err) => {
console.error(err)
(err: any) => {
Analytics.handleError(err)
isLoading = false
}
)
@@ -58,7 +58,6 @@
},
(err) => {
Analytics.handleError(err)
console.error(err)
isLoading = false
}
)
@@ -69,7 +68,6 @@
},
(err) => {
Analytics.handleError(err)
console.error(err)
isLoading = false
}
)
+1
View File
@@ -39,6 +39,7 @@
"typescript": "^5.8.3"
},
"dependencies": {
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/presentation": "^0.6.3",
"@hcengineering/setting-resources": "^0.6.0",
@@ -13,6 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Analytics } from '@hcengineering/analytics'
import { translate } from '@hcengineering/platform'
import { ExecutionError } from '@hcengineering/process'
import { Label } from '@hcengineering/ui'
@@ -24,8 +25,8 @@
for (const key in val.intlProps) {
try {
val.props[key] = await translate(val.intlProps[key], {})
} catch (err) {
console.error(err)
} catch (err: any) {
Analytics.handleError(err)
}
}
}
@@ -24,6 +24,7 @@
import { getContext } from '../../utils'
import ProcessAttribute from '../ProcessAttribute.svelte'
import { createEventDispatcher } from 'svelte'
import { Analytics } from '@hcengineering/analytics'
export let readonly: boolean
export let process: Process
@@ -57,11 +58,11 @@
.then((p) => {
editor = p
})
.catch((e) => {
console.error(e)
.catch((e: any) => {
Analytics.handleError(e)
})
} catch (e) {
console.error(e)
} catch (e: any) {
Analytics.handleError(e)
}
}
@@ -22,6 +22,7 @@
import view from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import ProcessAttribute from '../ProcessAttribute.svelte'
import { Analytics } from '@hcengineering/analytics'
export let func: ProcessFunction
export let process: Process
@@ -49,11 +50,11 @@
.then((p) => {
editor = p
})
.catch((e) => {
console.error(e)
.catch((e: any) => {
Analytics.handleError(e)
})
} catch (e) {
console.error(e)
} catch (e: any) {
Analytics.handleError(e)
}
}
@@ -22,6 +22,7 @@
import view from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import ProcessAttribute from '../ProcessAttribute.svelte'
import { Analytics } from '@hcengineering/analytics'
export let func: ProcessFunction
export let process: Process
@@ -49,11 +50,11 @@
.then((p) => {
editor = p
})
.catch((e) => {
console.error(e)
.catch((e: any) => {
Analytics.handleError(e)
})
} catch (e) {
console.error(e)
} catch (e: any) {
Analytics.handleError(e)
}
}
@@ -36,7 +36,8 @@
toIdMap,
TxProcessor,
WithLookup,
type Blob
type Blob,
type PersonId
} from '@hcengineering/core'
import { getMetadata, getResource, setPlatformStatus, unknownError } from '@hcengineering/platform'
import presentation, {
@@ -456,7 +457,6 @@
object.skills = [...object.skills, ...newSkills]
} catch (err: any) {
Analytics.handleError(err)
console.error(err)
}
}
@@ -464,8 +464,8 @@
if (object.resumeUuid) {
try {
await deleteFile(object.resumeUuid)
} catch (err) {
console.error(err)
} catch (err: any) {
Analytics.handleError(err)
}
}
}
@@ -22,6 +22,7 @@
import { IntlString, translateCB } from '@hcengineering/platform'
import contact, { getCurrentEmployee, SocialIdentityRef } from '@hcengineering/contact'
import { buildSocialIdString, SocialIdType } from '@hcengineering/core'
import { Analytics } from '@hcengineering/analytics'
export let mailboxOptions: MailboxOptions
@@ -83,6 +84,7 @@
loading = false
formatError(err)
console.error('Failed to create mailbox', err)
Analytics.handleError(err)
}
}
@@ -29,6 +29,7 @@
import { getAccountClient } from '../utils'
import contact, { getCurrentEmployee } from '@hcengineering/contact'
import { buildSocialIdString, SocialIdType } from '@hcengineering/core'
import { Analytics } from '@hcengineering/analytics'
export let mailbox: MailboxInfo
export let mailboxIdx: number
@@ -49,8 +50,9 @@
loadingRequested()
try {
await deleteMailbox()
} catch (err) {
} catch (err: any) {
console.error('Failed to delete mailbox', err)
Analytics.handleError(err)
}
reloadRequested()
}
@@ -30,6 +30,7 @@
import { onMount } from 'svelte'
import { MailboxInfo, MailboxOptions } from '@hcengineering/account-client'
import MailboxItem from './MailboxItem.svelte'
import { Analytics } from '@hcengineering/analytics'
let boxesLoading = true
let optionsLoading = true
@@ -61,6 +62,7 @@
.catch((err: any) => {
optionsLoading = false
console.error('Failed to load mailbox options', err)
Analytics.handleError(err)
})
}
@@ -22,6 +22,7 @@
import setting from '../plugin'
import { getAccountClient } from '../utils'
import { Analytics } from '@hcengineering/analytics'
const query = createQuery()
const currentAccount = getCurrentAccount()
@@ -61,8 +62,8 @@
try {
await accountClient.updateWorkspaceRole(personUuid, value)
workspaceMembers[personUuid] = value
} catch (e) {
console.error(e)
} catch (e: any) {
Analytics.handleError(e)
}
}
let search = ''
@@ -41,7 +41,6 @@
label = setting.string.Saved
} catch (e: any) {
Analytics.handleError(e)
console.error(e)
label = presentation.string.Save
saved = false
error = true
@@ -76,7 +76,7 @@
await disconnect(integration)
}
} catch (err: any) {
console.error('Error disconnecting integration:', err)
Analytics.handleError(err)
const errorMessage: string = err.message ?? 'Unknown error'
const integrationError = errorMessage.includes('Failed to fetch')
? setting.string.ServiceIsUnavailable
@@ -102,7 +102,7 @@
await disconnectAll(integration)
}
} catch (err: any) {
console.error('Error disconnecting integration:', err)
Analytics.handleError(err)
const errorMessage: string = err.message ?? 'Unknown error'
const integrationError = errorMessage.includes('Failed to fetch')
? setting.string.ServiceIsUnavailable
@@ -35,6 +35,7 @@
import IntegrationCard from './IntegrationCard.svelte'
import IntegrationErrorNotification from './IntegrationErrorNotification.svelte'
import { getAccountClient } from '../../utils'
import { Analytics } from '@hcengineering/analytics'
const typeQuery = createQuery()
@@ -76,7 +77,7 @@
if (error != null) {
const decodedError = decodeURIComponent(error)
console.error('Integration error:', decodedError)
Analytics.handleError(new Error(`Integration error: ${decodedError}`))
await showErrorNotification(decodedError)
// Clean up integrationError parameter from the URL
urlParams.delete('integrationError')
@@ -89,8 +90,8 @@
}
subscribe()
startRefreshTimer()
} catch (err) {
console.error('Error loading integrations:', err)
} catch (err: any) {
Analytics.handleError(err)
await showLoadErrorNotification()
connections = []
integrations = []
@@ -149,8 +150,8 @@
connections = await accountClient.listIntegrations({ workspaceUuid: null })
integrations = await accountClient.listIntegrations({ workspaceUuid: workspace })
lastEventTime = Date.now() // Update last event time after successful refresh
} catch (err) {
console.error('Error refreshing integrations:', err)
} catch (err: any) {
Analytics.handleError(err)
}
})()
@@ -24,6 +24,7 @@
import AddSocialId from './AddSocialId.svelte'
import setting from '../../plugin'
import { getAccountClient } from '../../utils'
import { Analytics } from '@hcengineering/analytics'
export let provider: SocialIdentityProvider
export let onAdded: (socialId: SocialId) => void | Promise<void>
@@ -62,7 +63,7 @@
retryOn = otpInfo.retryOn
canResend = false
} catch (err: any) {
console.error(err)
Analytics.handleError(err)
if (err instanceof PlatformError) {
status = err.status
} else {
@@ -97,7 +98,9 @@
const newSocialId = socialIds.find((it) => it._id === newSocialIdLoginInfo.socialId)
if (newSocialId == null) {
console.error(`New social id ${newSocialIdLoginInfo.socialId} not found in the updated social ids list`)
Analytics.handleError(
new Error(`New social id ${newSocialIdLoginInfo.socialId} not found in the updated social ids list`)
)
return
}
@@ -142,7 +145,7 @@
}
dispatch('close')
} catch (err: any) {
console.error(err)
Analytics.handleError(err)
if (err instanceof PlatformError) {
status = err.status
} else {
@@ -29,6 +29,7 @@
import setting from '../../plugin'
import { getAccountClient } from '../../utils'
import { Analytics } from '@hcengineering/analytics'
export let socialId: SocialId
export let socialIdProvider: SocialIdentityProvider
@@ -79,7 +80,7 @@
currAcc = updatedAccount
dispatch('released')
} catch (err: any) {
console.error(err)
Analytics.handleError(err)
void setPlatformStatus(unknownError(err))
}
}
+1
View File
@@ -38,6 +38,7 @@
"svelte-eslint-parser": "^0.33.1"
},
"dependencies": {
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/core": "^0.6.32",
"@hcengineering/panel": "^0.6.23",
"@hcengineering/platform": "^0.6.11",
@@ -24,6 +24,7 @@
import SurveyPopup from './SurveyPopup.svelte'
import survey from '../plugin'
import { makePollData } from '../utils'
import { Analytics } from '@hcengineering/analytics'
export let objectId: Ref<Doc>
export let space: Ref<Space>
@@ -49,7 +50,7 @@
const poll = await client.findOne(survey.class.Poll, { _id: pollId })
if (poll === undefined) {
console.error(`Could not find just created poll ${pollId}.`)
Analytics.handleError(new Error(`Could not find just created poll ${pollId}.`))
return
}
@@ -17,6 +17,7 @@
import { Label } from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation'
import { IntlString } from '@hcengineering/platform'
import { Analytics } from '@hcengineering/analytics'
export let value: Association
@@ -26,8 +27,8 @@
try {
const _classLabel = hierarchy.getClass(_class)
return _classLabel.label
} catch (err) {
console.error(err)
} catch (err: any) {
Analytics.handleError(err)
return core.string.Class
}
}
@@ -19,25 +19,26 @@
import { getMetadata, getResource } from '@hcengineering/platform'
import presentation, { decodeTokenPayload, isAdminUser } from '@hcengineering/presentation'
import {
Icon,
IconCheck,
Label,
Loading,
Location,
SearchEdit,
closePopup,
fetchMetadataLocalStorage,
getCurrentLocation,
Icon,
IconCheck,
isSameSegments,
Label,
Loading,
Location,
locationStorageKeyId,
locationToUrl,
navigate,
resolvedLocationStore,
SearchEdit,
ticker
} from '@hcengineering/ui'
import { workbenchId } from '@hcengineering/workbench'
import { onDestroy, onMount } from 'svelte'
import { Analytics } from '@hcengineering/analytics'
import { workspacesStore } from '../utils'
// import Drag from './icons/Drag.svelte'
@@ -124,8 +125,8 @@
.then(async (json) => {
data = await json.json()
})
.catch((err) => {
console.error(err)
.catch((err: any) => {
Analytics.handleError(err)
})
})
)
@@ -344,7 +344,6 @@
return await titleProvider(client, _id)
} catch (err: any) {
Analytics.handleError(err)
console.error(err)
}
}
-3
View File
@@ -32,15 +32,12 @@ startFront(metricsContext, {
INTERCOM_APP_ID: process.env.INTERCOM_APP_ID ?? '',
INTERCOM_API_URL: process.env.INTERCOM_API_URL ?? '',
GITHUB_URL: process.env.GITHUB_URL ?? '',
SENTRY_DSN: process.env.SENTRY_DSN ?? '',
LIVEKIT_WS: process.env.LIVEKIT_WS ?? '',
LOVE_ENDPOINT: process.env.LOVE_ENDPOINT ?? '',
SIGN_URL: process.env.SIGN_URL ?? '',
PRINT_URL: process.env.PRINT_URL ?? '',
PRESENCE_URL: process.env.PRESENCE_URL ?? '',
PUSH_PUBLIC_KEY: process.env.PUSH_PUBLIC_KEY ?? '',
POSTHOG_API_KEY: process.env.POSTHOG_API_KEY,
POSTHOG_HOST: process.env.POSTHOG_HOST,
DESKTOP_UPDATES_URL: process.env.DESKTOP_UPDATES_URL,
DESKTOP_UPDATES_CHANNEL: process.env.DESKTOP_UPDATES_CHANNEL,
DESKTOP_UPDATES_CHANNELS: process.env.DESKTOP_UPDATES_CHANNELS,
@@ -18,9 +18,12 @@ export interface Config {
Secret: string
ServiceID: string
AccountsUrl: string
PostHogHost: string
PostHogAPI: string
SentryDSN?: string
// Optional PostHog configuration
// If posthog is not configured, will use OTLP for send events as measurements and errors as errors
PostHogHost?: string
PostHogAPI?: string
MaxPayloadSize?: string
}
@@ -34,11 +37,12 @@ const config: Config = (() => {
AccountsUrl: process.env.ACCOUNTS_URL,
PostHogHost: process.env.POSTHOG_HOST,
PostHogAPI: process.env.POSTHOG_API_KEY,
SentryDSN: process.env.SENTRY_DSN ?? '',
MaxPayloadSize: process.env.MAX_PAYLOAD_SIZE ?? '10mb'
}
const missingEnv = (Object.keys(params) as Array<keyof Config>).filter((key) => params[key] === undefined)
const requiredParams = ['Secret', 'AccountsUrl'] as Array<keyof Config>
const missingEnv = requiredParams.filter((key) => params[key] === undefined)
if (missingEnv.length > 0) {
throw Error(`Missing config for attributes: ${missingEnv.join(', ')}`)
@@ -13,10 +13,10 @@
// limitations under the License.
//
import { type MeasureContext } from '@hcengineering/core'
import * as fs from 'fs'
import * as maxmind from 'maxmind'
import * as path from 'path'
import * as fs from 'fs'
import { type MeasureContext } from '@hcengineering/core'
// MaxMind reader instances
let cityReader: maxmind.Reader<maxmind.CityResponse> | null = null
@@ -17,4 +17,6 @@ import { config } from 'dotenv'
import { main } from './main'
config()
void main()
void main().catch((err) => {
console.error('Error occurred while starting the application:', err)
})
@@ -56,7 +56,7 @@ export const main = async (): Promise<void> => {
accountsUrl: config.AccountsUrl
})
const app = createServer()
const app = createServer(ctx)
const server = listen(app, config.Port)
const shutdown = (): void => {
@@ -1,22 +0,0 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Client } from '@hcengineering/core'
import { createClient, getTransactorEndpoint } from '@hcengineering/server-client'
export async function connectPlatform (token: string): Promise<Client> {
const endpoint = await getTransactorEndpoint(token)
return await createClient(endpoint, token)
}
@@ -13,15 +13,16 @@
// limitations under the License.
//
import { AnalyticEvent, AnalyticEventType } from '@hcengineering/analytics-collector'
import type { MeasureContext } from '@hcengineering/core'
import { extractToken } from '@hcengineering/server-client'
import { Token } from '@hcengineering/server-token'
import cors from 'cors'
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
import { type Server } from 'http'
import { AnalyticEvent, AnalyticEventType } from '@hcengineering/analytics-collector'
import { extractToken } from '@hcengineering/server-client'
import config from './config'
import { ApiError } from './error'
import { getGeoLocationFromIp, getClientIp, geoFieldMapping, getAllPossibleIps } from './geoip'
import { geoFieldMapping, getAllPossibleIps, getClientIp, getGeoLocationFromIp } from './geoip'
type AsyncRequestHandler = (req: Request, res: Response, token: Token, next: NextFunction) => Promise<void>
@@ -236,7 +237,7 @@ async function preparePostHogEvent (event: AnalyticEvent, req: Request): Promise
return regularEventForPostHog
}
export function createServer (): Express {
export function createServer (ctx: MeasureContext): Express {
const app = express()
app.use(cors())
app.use(express.json({ limit: config.MaxPayloadSize }))
@@ -256,48 +257,23 @@ export function createServer (): Express {
const payloadSize = JSON.stringify(req.body).length
console.log(`Received batch: ${events.length} events, ${payloadSize} bytes`)
const posthogEvents = await Promise.all(
events.map(async (event) => {
return await preparePostHogEvent(event, req)
})
)
const payload = {
api_key: config.PostHogAPI,
batch: posthogEvents.reverse()
}
const posthogPayloadSize = JSON.stringify(payload).length
console.log(`Sending to PostHog: ${posthogEvents.length} events, ${posthogPayloadSize} bytes`)
try {
const response = await fetch(`${config.PostHogHost}/batch/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Analytics-Collector/1.0'
},
body: JSON.stringify(payload)
})
if (!response.ok) {
const errorText = await response.text()
console.error(`PostHog API error: ${response.status} ${response.statusText}`, errorText)
} else {
console.log(`Successfully sent ${posthogEvents.length} events to PostHog`)
}
} catch (error) {
console.error('Failed to send events to PostHog:', error)
}
res.status(200)
res.json({
received: events.length,
processed: posthogEvents.length,
payloadSize,
timestamp: new Date().toISOString()
})
res.json({})
if (config.PostHogHost !== undefined) {
void sendEventsToPosthog(events, req).catch((err) => {
console.error('Error sending events to PostHog:', err)
})
}
for (const evt of events) {
if (evt.event === AnalyticEventType.Error) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { error_message, error_type, error_stack } = evt.properties ?? {}
ctx.error(error_message ?? '', { error_type, error_stack })
}
// TODO: Handle other events as gauges per minute
}
})
)
@@ -314,6 +290,41 @@ export function createServer (): Express {
return app
}
async function sendEventsToPosthog (events: AnalyticEvent[], req: Request): Promise<void> {
const posthogEvents: Record<string, any>[] = []
for (const evt of events) {
posthogEvents.push(await preparePostHogEvent(evt, req))
}
const payload = {
api_key: config.PostHogAPI,
batch: posthogEvents.reverse()
}
const posthogPayloadSize = JSON.stringify(payload).length
console.log(`Sending to PostHog: ${posthogEvents.length} events, ${posthogPayloadSize} bytes`)
try {
const response = await fetch(`${config.PostHogHost}/batch/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Analytics-Collector/1.0'
},
body: JSON.stringify(payload)
})
if (!response.ok) {
const errorText = await response.text()
console.error(`PostHog API error: ${response.status} ${response.statusText}`, errorText)
} else {
console.log(`Successfully sent ${posthogEvents.length} events to PostHog`)
}
} catch (error) {
console.error('Failed to send events to PostHog:', error)
}
}
export function listen (e: Express, port: number, host?: string): Server {
const cb = (): void => {
console.log(`Analytics collector service has been started at ${host ?? '*'}:${port}`)
@@ -18,7 +18,6 @@ dotenvConfig()
interface Config {
secret: string
sentryDSN: string
accountsUrl: string
smtpUrl: string
queueRegion: string
@@ -26,7 +25,6 @@ interface Config {
const config: Config = {
secret: process.env.SECRET ?? 'secret',
sentryDSN: process.env.SENTRY_DSN ?? '',
accountsUrl: process.env.ACCOUNTS_URL ?? 'http://localhost:3000',
smtpUrl: process.env.SMTP_URL ?? 'http://localhost:8097',
queueRegion: process.env.QUEUE_REGION ?? 'localhost'
-3
View File
@@ -22,7 +22,6 @@ interface Config {
BotName: string
SentryDSN: string
BrandingPath: string
WorkspaceInactivityInterval: number // Interval in days to stop workspace synchronization if not visited
@@ -49,7 +48,6 @@ const envMap: { [key in keyof Config]: string } = {
CollaboratorURL: 'COLLABORATOR_URL',
SentryDSN: 'SENTRY_DSN',
BrandingPath: 'BRANDING_PATH',
WorkspaceInactivityInterval: 'WORKSPACE_INACTIVITY_INTERVAL',
@@ -94,7 +92,6 @@ const config: Config = (() => {
CollaboratorURL: process.env[envMap.CollaboratorURL],
SentryDSN: process.env[envMap.SentryDSN],
BrandingPath: process.env[envMap.BrandingPath] ?? '',
WorkspaceInactivityInterval: parseInt(process.env[envMap.WorkspaceInactivityInterval] ?? '3'), // In days
RateLimit: parseInt(process.env[envMap.RateLimit] ?? '25')
+10 -4
View File
@@ -2,21 +2,22 @@
## Overview
The Mail Service is responsible for sending emails using SMTP or SES transfer.
The Mail Service is responsible for sending emails using SMTP or SES transfer.
It supports sending emails with multiple recipients, along with optional CC, BCC, and HTML content.
### Configuration
Environment variables should be set to configure the Mail Service:
- `PORT`: The port on which the mail service listens for incoming HTTP requests.
- `API_KEY`: An API key that clients must pass. The parameter is optional, should be provided when external access to the service is allowed.
- `SENTRY_DSN`: (optional) The Data Source Name for Sentry error tracking. This is optional and should be set if you wish to enable error tracking with Sentry.
- `SOURCE`: The sender source (fallback for when emails emit from the system).
- `REPLY_TO`: (optional) Email to use for replies (useful for uni-directional STMP setups where emails are only emitted, but not received).
Settings for SMTP or SES email service should be specified, simultaneous use of both protocols is not supported
SMTP settings:
- `SMTP_HOST`: Hostname of the SMTP server used for sending emails.
- `SMTP_PORT`: Port number of the SMTP server.
- `SMTP_USERNAME`: Username for authenticating with the SMTP server. Refer to your SMTP server documentation for the appropriate format.
@@ -29,6 +30,7 @@ SMTP settings:
- `SMTP_ALLOW_SELF_SIGNED` (Optional): Allow self-signed certificates for TLS connections. Set to 'true' to enable (not recommended for production use). Default: false
SES settings:
- `SES_ACCESS_KEY`: AWS SES access key for authentication.
- `SES_SECRET_KEY`: AWS SES secret key for authentication.
- `SES_REGION`: AWS SES region where your SES service is hosted.
@@ -36,14 +38,17 @@ SES settings:
### Running the Service
Add .env file to the root of the project with the following content to add integration with fake SMTP server:
```
PORT=8097
SMTP_HOST="mail.smtpbucket.com"
SMTP_PORT=8025
SMTP_PORT=8025
```
To use the real SMTP server it is required to register an account in some email service provider and specify settings and credentials for it.
Start the service locally using:
```bash
rushx run-local
```
@@ -76,6 +81,7 @@ Send an email message.
- `raw`: An optional special value that overrides the entire contents of the current MIME node, including MIME headers. Useful if you want to prepare node contents yourself.
Request body example:
```
{
"subject": "Test SMTP",
@@ -87,7 +93,7 @@ Request body example:
"filename": "test.txt",
"content": "Hello world",
"contentType": "text/plain"
}
}
]
}
```
@@ -27,7 +27,6 @@ export interface Config {
QueueConfig: string
QueueRegion: string
Secret: string
SentryDSN: string
ServiceId: string
}
@@ -46,7 +45,6 @@ const config: Config = (() => {
App: process.env.APP ?? 'Huly',
OtpTimeToLiveSec: parseNumber(process.env.OTP_TIME_TO_LIVE_SEC) ?? 5 * 60,
OtpRetryDelaySec: parseNumber(process.env.OTP_RETRY_DELAY_SEC) ?? 60,
SentryDSN: process.env.SENTRY_DSN ?? '',
AccountsURL: process.env.ACCOUNTS_URL,
DbUrl: process.env.DB_URL,
QueueRegion: process.env.QUEUE_REGION,