diff --git a/components/gitpod-protocol/src/analytics.ts b/components/gitpod-protocol/src/analytics.ts new file mode 100644 index 00000000000000..2a2526e7793f2a --- /dev/null +++ b/components/gitpod-protocol/src/analytics.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2021 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License-AGPL.txt in the project root for license information. + */ + +import { Without } from "./util/without"; + +export const IAnalyticsWriter = Symbol("IAnalyticsWriter"); + +type Identity = + | { userId: string | number } + | { userId?: string | number; anonymousId: string | number }; + +interface Message { + messageId?: string; +} + +export type IdentifyMessage = Message & Identity & { + traits?: any; + timestamp?: Date; + context?: any; +}; + +export type TrackMessage = Message & Identity & { + event: string; + properties?: any; + timestamp?: Date; + context?: any; +}; + +export type RemoteTrackMessage = Without; + +export interface IAnalyticsWriter { + + identify(msg: IdentifyMessage): void; + + track(msg: TrackMessage): void; + +} diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts index 831b84cda466a8..bfc56ba173cb34 100644 --- a/components/gitpod-protocol/src/gitpod-service.ts +++ b/components/gitpod-protocol/src/gitpod-service.ts @@ -25,6 +25,7 @@ import { Emitter } from './util/event'; import { AccountStatement, CreditAlert } from './accounting-protocol'; import { GithubUpgradeURL, PlanCoupon } from './payment-protocol'; import { TeamSubscription, TeamSubscriptionSlot, TeamSubscriptionSlotResolved } from './team-subscription-protocol'; +import { RemoteTrackMessage } from './analytics'; export interface GitpodClient { onInstanceUpdate(instance: WorkspaceInstance): void; @@ -212,6 +213,11 @@ export interface GitpodServer extends JsonRpcServer, AdminServer, createProject(params: CreateProjectParams): Promise; getProjects(teamId: string): Promise; getPrebuilds(teamId: string, project: string): Promise; + + /** + * Analytics + */ + trackEvent(event: RemoteTrackMessage): Promise; } export interface CreateProjectParams { diff --git a/components/gitpod-protocol/src/util/analytics.ts b/components/gitpod-protocol/src/util/analytics.ts index c6e6825b2b8d40..87881cff9dae23 100644 --- a/components/gitpod-protocol/src/util/analytics.ts +++ b/components/gitpod-protocol/src/util/analytics.ts @@ -5,30 +5,9 @@ */ import Analytics = require("analytics-node"); +import { IAnalyticsWriter, IdentifyMessage, TrackMessage } from "../analytics"; import { log } from './logging'; -export const IAnalyticsWriter = Symbol("IAnalyticsWriter"); - -type Identity = - | { userId: string | number } - | { userId?: string | number; anonymousId: string | number }; - -interface Message { - messageId?: string; -} - -export type IdentifyMessage = Message & Identity & { - traits?: any; - timestamp?: Date; - context?: any; -}; - -export type TrackMessage = Message & Identity & { - event: string; - properties?: any; - timestamp?: Date; - context?: any; -}; export function newAnalyticsWriterFromEnv(): IAnalyticsWriter { switch (process.env.GITPOD_ANALYTICS_WRITER) { @@ -41,14 +20,6 @@ export function newAnalyticsWriterFromEnv(): IAnalyticsWriter { } } -export interface IAnalyticsWriter { - - identify(msg: IdentifyMessage): void; - - track(msg: TrackMessage): void; - -} - class SegmentAnalyticsWriter implements IAnalyticsWriter { protected readonly analytics: Analytics; diff --git a/components/server/src/auth/login-completion-handler.ts b/components/server/src/auth/login-completion-handler.ts index 34271334fce148..34865ca4881bc2 100644 --- a/components/server/src/auth/login-completion-handler.ts +++ b/components/server/src/auth/login-completion-handler.ts @@ -15,7 +15,7 @@ import { HostContextProvider } from './host-context-provider'; import { AuthProviderService } from './auth-provider-service'; import { TosFlow } from '../terms/tos-flow'; import { increaseLoginCounter } from '../../src/prometheus-metrics'; -import { IAnalyticsWriter } from '@gitpod/gitpod-protocol/lib/util/analytics'; +import { IAnalyticsWriter } from '@gitpod/gitpod-protocol/lib/analytics'; /** * The login completion handler pulls the strings between the OAuth2 flow, the ToS flow, and the session management. diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index f11c5bbf614a0c..af460bbc51b1fa 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -163,6 +163,8 @@ function readConfig(): RateLimiterConfig { "createProject": { group: "default", points: 1 }, "getProjects": { group: "default", points: 1 }, "getPrebuilds": { group: "default", points: 1 }, + + "trackEvent": { group: "default", points: 1 }, }; const fromEnv = JSON.parse(process.env.RATE_LIMITER_CONFIG || "{}") diff --git a/components/server/src/container-module.ts b/components/server/src/container-module.ts index a5423ab0886e0a..34df95830ca32b 100644 --- a/components/server/src/container-module.ts +++ b/components/server/src/container-module.ts @@ -71,12 +71,13 @@ import { ContentServiceStorageClient } from './storage/content-service-client'; import { IDEPluginServiceClient } from '@gitpod/content-service/lib/ideplugin_grpc_pb'; import { GitTokenScopeGuesser } from './workspace/git-token-scope-guesser'; import { GitTokenValidator } from './workspace/git-token-validator'; -import { newAnalyticsWriterFromEnv, IAnalyticsWriter } from '@gitpod/gitpod-protocol/lib/util/analytics'; +import { newAnalyticsWriterFromEnv } from '@gitpod/gitpod-protocol/lib/util/analytics'; import { OAuthController } from './oauth-server/oauth-controller'; import { ImageBuildPrefixContextParser } from './workspace/imagebuild-prefix-context-parser'; import { AdditionalContentPrefixContextParser } from './workspace/additional-content-prefix-context-parser'; import { WorkspaceLogService } from './workspace/workspace-log-service'; import { HeadlessLogController } from './workspace/headless-log-controller'; +import { IAnalyticsWriter } from '@gitpod/gitpod-protocol/lib/analytics'; export const productionContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => { bind(Env).toSelf().inSingletonScope(); diff --git a/components/server/src/user/user-controller.ts b/components/server/src/user/user-controller.ts index 7c3e0c3621a62d..8cb1d8804a626e 100644 --- a/components/server/src/user/user-controller.ts +++ b/components/server/src/user/user-controller.ts @@ -24,7 +24,7 @@ import { GitpodToken, GitpodTokenType, User } from "@gitpod/gitpod-protocol"; import { HostContextProvider } from "../auth/host-context-provider"; import { AuthFlow } from "../auth/auth-provider"; import { LoginCompletionHandler } from "../auth/login-completion-handler"; -import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/util/analytics"; +import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics"; import { TosCookie } from "./tos-cookie"; import { TosFlow } from "../terms/tos-flow"; import { increaseLoginCounter } from '../../src/prometheus-metrics'; diff --git a/components/server/src/user/user-deletion-service.ts b/components/server/src/user/user-deletion-service.ts index 96bc2edd0e384c..90fd4b420a5df9 100644 --- a/components/server/src/user/user-deletion-service.ts +++ b/components/server/src/user/user-deletion-service.ts @@ -14,7 +14,7 @@ import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-pr import { StopWorkspaceRequest, StopWorkspacePolicy } from "@gitpod/ws-manager/lib"; import { WorkspaceDeletionService } from "../workspace/workspace-deletion-service"; import { AuthProviderService } from "../auth/auth-provider-service"; -import { IAnalyticsWriter } from '@gitpod/gitpod-protocol/lib/util/analytics'; +import { IAnalyticsWriter } from '@gitpod/gitpod-protocol/lib/analytics'; @injectable() export class UserDeletionService { diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index fb95d7c2849e43..59179f9917894b 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -17,6 +17,7 @@ import { TeamSubscription, TeamSubscriptionSlot, TeamSubscriptionSlotResolved } import { Cancelable } from '@gitpod/gitpod-protocol/lib/util/cancelable'; import { log, LogContext } from '@gitpod/gitpod-protocol/lib/util/logging'; import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing'; +import { RemoteTrackMessage, TrackMessage } from '@gitpod/gitpod-protocol/lib/analytics'; import { ImageBuilderClientProvider, LogsRequest } from '@gitpod/image-builder/lib'; import { WorkspaceManagerClientProvider } from '@gitpod/ws-manager/lib/client-provider'; import { ControlPortRequest, DescribeWorkspaceRequest, MarkActiveRequest, PortSpec, PortVisibility as ProtoPortVisibility, StopWorkspacePolicy, StopWorkspaceRequest } from '@gitpod/ws-manager/lib/core_pb'; @@ -26,7 +27,7 @@ import * as opentracing from 'opentracing'; import { URL } from 'url'; import * as uuidv4 from 'uuid/v4'; import { Disposable, ResponseError } from 'vscode-jsonrpc'; -import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/util/analytics"; +import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics"; import { AuthProviderService } from '../auth/auth-provider-service'; import { HostContextProvider } from '../auth/host-context-provider'; import { GuardedResource, ResourceAccessGuard, ResourceAccessOp } from '../auth/resource-access'; @@ -1775,6 +1776,27 @@ export class GitpodServerImpl { + if (!this.user) { + // we cannot track events if don't know the user, because we have no sensible means + // to produce a correlatable anonymousId. + return; + } + + // Beware: DO NOT just event... the message, but consume it individually as the message is coming from + // the wire and we have no idea what's in it. Even passing the context and properties directly + // is questionable. Considering we're handing down the msg and do not know how the analytics library + // handles potentially broken or malicious input, we better err on the side of caution. + const msg: TrackMessage = { + userId: this.user.id, + event: event.event, + messageId: event.messageId, + context: event.context, + properties: event.properties, + } + this.analytics.track(msg); + } + async getTerms(): Promise { // Terms are publicly available, thus no user check here. diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index b9ad46cc4b2b38..cc54bbb9bae271 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -8,7 +8,7 @@ import { CloneTargetMode, FileDownloadInitializer, GitAuthMethod, GitConfig, Git import { CompositeInitializer, FromBackupInitializer } from "@gitpod/content-service/lib/initializer_pb"; import { DBUser, DBWithTracing, TracedUserDB, TracedWorkspaceDB, UserDB, WorkspaceDB } from '@gitpod/gitpod-db/lib'; import { CommitContext, Disposable, GitpodToken, GitpodTokenType, IssueContext, NamedWorkspaceFeatureFlag, PullRequestContext, RefType, SnapshotContext, StartWorkspaceResult, User, UserEnvVar, UserEnvVarValue, WithEnvvarsContext, WithPrebuild, Workspace, WorkspaceContext, WorkspaceImageSource, WorkspaceImageSourceDocker, WorkspaceImageSourceReference, WorkspaceInstance, WorkspaceInstanceConfiguration, WorkspaceInstanceStatus, WorkspaceProbeContext, Permission, HeadlessLogEvent, HeadlessWorkspaceEventType, DisposableCollection, AdditionalContentContext, ImageConfigFile, EmailType } from "@gitpod/gitpod-protocol"; -import { IAnalyticsWriter } from '@gitpod/gitpod-protocol/lib/util/analytics'; +import { IAnalyticsWriter } from '@gitpod/gitpod-protocol/lib/analytics'; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import { BuildRegistryAuth, BuildRegistryAuthSelective, BuildRegistryAuthTotal, BuildRequest, BuildResponse, BuildSource, BuildSourceDockerfile, BuildSourceReference, BuildStatus, ImageBuilderClientProvider, ResolveBaseImageRequest, ResolveWorkspaceImageRequest } from "@gitpod/image-builder/lib"; diff --git a/components/ws-manager-bridge/src/bridge.ts b/components/ws-manager-bridge/src/bridge.ts index 0b0297bd4f6faf..773ccfef890f8f 100644 --- a/components/ws-manager-bridge/src/bridge.ts +++ b/components/ws-manager-bridge/src/bridge.ts @@ -13,7 +13,7 @@ import { UserDB } from "@gitpod/gitpod-db/lib/user-db"; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { HeadlessLogEvent } from "@gitpod/gitpod-protocol/lib/headless-workspace-log"; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; -import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/util/analytics"; +import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics"; import { TracedWorkspaceDB, TracedUserDB, DBWithTracing } from '@gitpod/gitpod-db/lib/traced-db'; import { PrometheusMetricsExporter } from "./prometheus-metrics-exporter"; import { ClientProvider, WsmanSubscriber } from "./wsman-subscriber"; diff --git a/components/ws-manager-bridge/src/container-module.ts b/components/ws-manager-bridge/src/container-module.ts index e02a9cc021eeba..c3db87a694230c 100644 --- a/components/ws-manager-bridge/src/container-module.ts +++ b/components/ws-manager-bridge/src/container-module.ts @@ -20,7 +20,8 @@ import { filePathTelepresenceAware } from '@gitpod/gitpod-protocol/lib/env'; import { WorkspaceManagerClientProvider } from '@gitpod/ws-manager/lib/client-provider'; import { WorkspaceManagerClientProviderCompositeSource, WorkspaceManagerClientProviderDBSource, WorkspaceManagerClientProviderSource } from '@gitpod/ws-manager/lib/client-provider-source'; import { ClusterService, ClusterServiceServer } from './cluster-service-server'; -import { IAnalyticsWriter, newAnalyticsWriterFromEnv } from '@gitpod/gitpod-protocol/lib/util/analytics'; +import { IAnalyticsWriter } from '@gitpod/gitpod-protocol/lib/analytics'; +import { newAnalyticsWriterFromEnv } from '@gitpod/gitpod-protocol/lib/util/analytics'; export const containerModule = new ContainerModule(bind => {