Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Update Segment identify calls to remove email, add domain #405

Merged
merged 4 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/authProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { getStorageManager } from "./storage";
import { AUTH_COMPLETED_KEY, AUTH_SESSION_EXISTS_KEY } from "./storage/constants";
import { getResourceManager } from "./storage/resourceManager";
import { getUriHandler } from "./uriHandler";
import { getTelemetryLogger } from "./telemetry";
import { sendTelemetryIdentifyEvent } from "./telemetry/telemetry";

const logger = new Logger("authProvider");

Expand Down Expand Up @@ -154,9 +154,10 @@ export class ConfluentCloudAuthProvider implements vscode.AuthenticationProvider

// User logged in successfully so we send an identify event to Segment
if (authenticatedConnection.status.authentication.user) {
getTelemetryLogger().logUsage("Signed In", {
identify: true,
user: authenticatedConnection.status.authentication.user,
sendTelemetryIdentifyEvent({
eventName: "Signed In",
userInfo: authenticatedConnection.status.authentication.user,
session: undefined,
});
}
// we want to continue regardless of whether or not the user dismisses the notification,
Expand Down
2 changes: 1 addition & 1 deletion src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Sentry from "@sentry/node";
import * as vscode from "vscode";
import { Logger } from "../logging";
import { getTelemetryLogger } from "../telemetry";
import { getTelemetryLogger } from "../telemetry/telemetryLogger";

const logger = new Logger("commands");

Expand Down
2 changes: 1 addition & 1 deletion src/consume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { topicQuickPick } from "./quickpicks/topics";
import { scheduler } from "./scheduler";
import { getSidecar, type SidecarHandle } from "./sidecar";
import { BitSet, includesSubstring, Stream } from "./stream/stream";
import { getTelemetryLogger } from "./telemetry";
import { getTelemetryLogger } from "./telemetry/telemetryLogger";
import { handleWebviewMessage } from "./webview/comms/comms";
import { type post } from "./webview/message-viewer";
import messageViewerTemplate from "./webview/message-viewer.html";
Expand Down
2 changes: 1 addition & 1 deletion src/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ConfluentCloudAuthProvider } from "./authProvider";
import { ExtensionContextNotSetError } from "./errors";
import { StorageManager } from "./storage";
import { ResourceManager } from "./storage/resourceManager";
import { checkTelemetrySettings } from "./telemetry";
import { checkTelemetrySettings } from "./telemetry/telemetry";
import { ResourceViewProvider } from "./viewProviders/resources";
import { SchemasViewProvider } from "./viewProviders/schemas";
import { TopicViewProvider } from "./viewProviders/topics";
Expand Down
16 changes: 13 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ if (process.env.SENTRY_DSN) {
}

import * as vscode from "vscode";
import { checkTelemetrySettings } from "./telemetry";
import { checkTelemetrySettings } from "./telemetry/telemetry";
if (process.env.SENTRY_DSN) {
Sentry.addEventProcessor(checkTelemetrySettings);
}
Expand Down Expand Up @@ -63,7 +63,8 @@ import { getCCloudAuthSession } from "./sidecar/connections";
import { StorageManager } from "./storage";
import { CCloudResourcePreloader } from "./storage/ccloudPreloader";
import { migrateStorageIfNeeded } from "./storage/migrationManager";
import { getTelemetryLogger } from "./telemetry";
import { getTelemetryLogger } from "./telemetry/telemetryLogger";
import { sendTelemetryIdentifyEvent } from "./telemetry/telemetry";
import { getUriHandler } from "./uriHandler";
import { ResourceViewProvider } from "./viewProviders/resources";
import { SchemasViewProvider } from "./viewProviders/schemas";
Expand Down Expand Up @@ -251,7 +252,16 @@ async function setupAuthProvider(): Promise<vscode.Disposable[]> {
]);

// attempt to get a session to trigger the initial auth badge for signing in
await getCCloudAuthSession();
const cloudSession = await getCCloudAuthSession();

// Send an Identify event to Segment with the session info if available
if (cloudSession) {
sendTelemetryIdentifyEvent({
eventName: "Activated With Session",
userInfo: undefined,
session: cloudSession,
});
}

return disposables;
}
Expand Down
2 changes: 1 addition & 1 deletion src/scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { getSidecar } from "./sidecar";

import { ExtensionContext, Uri, ViewColumn } from "vscode";
import { registerCommandWithLogging } from "./commands";
import { getTelemetryLogger } from "./telemetry";
import { getTelemetryLogger } from "./telemetry/telemetryLogger";
import { WebviewPanelCache } from "./webview-cache";
import { handleWebviewMessage } from "./webview/comms/comms";
import { type post } from "./webview/scaffold-form";
Expand Down
112 changes: 112 additions & 0 deletions src/telemetry/telemetry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { sendTelemetryIdentifyEvent } from "./telemetry";
import * as vscode from "vscode";
import * as telemetry from "./telemetryLogger";
import Sinon from "sinon";

describe("sendTelemetryIdentifyEvent", () => {
let mockTelemetryLogger: any;
let logUsageStub: Sinon.SinonStub;
let sdbx: sinon.SinonSandbox;

beforeEach(() => {
sdbx = Sinon.createSandbox();
mockTelemetryLogger = {
logUsage: sdbx.stub(),
};
sdbx.stub(telemetry, "getTelemetryLogger").returns(mockTelemetryLogger);
logUsageStub = mockTelemetryLogger.logUsage;
});

afterEach(() => {
sdbx.restore();
});

it("should send to logUsage with correct user info and domain", () => {
const userInfo = {
id: "user123",
username: "user@mycooldomain.com",
social_connection: "github",
};
const session = undefined;

sendTelemetryIdentifyEvent({
eventName: "testEvent",
userInfo,
session,
});

Sinon.assert.called(logUsageStub);
Sinon.assert.calledWith(logUsageStub, "testEvent", {
identify: true,
user: {
id: "user123",
domain: "mycooldomain.com",
social_connection: "github",
},
});
});
it("should send to logUsage with correct session info and domain", () => {
const userInfo = undefined;
const session = {
account: {
id: "session123",
label: "session@example.com",
},
} as vscode.AuthenticationSession;

sendTelemetryIdentifyEvent({
eventName: "testEvent",
userInfo,
session,
});

Sinon.assert.called(logUsageStub);
Sinon.assert.calledWith(logUsageStub, "testEvent", {
identify: true,
user: {
id: "session123",
domain: "example.com",
social_connection: undefined,
},
});
});
it("should not send to logUsage if user id is not available", () => {
const userInfo = {
id: undefined,
username: "user@example.com",
social_connection: "github",
};
const session = undefined;

sendTelemetryIdentifyEvent({
eventName: "testEvent",
userInfo,
session,
});

Sinon.assert.notCalled(logUsageStub);
});
it("should send event, no domain if username doesn't match email regex", () => {
const userInfo = {
id: "user1",
username: "glitchintheusername",
social_connection: "github",
};
const session = undefined;

sendTelemetryIdentifyEvent({
eventName: "testEvent",
userInfo,
session,
});

Sinon.assert.calledWith(logUsageStub, "testEvent", {
identify: true,
user: {
id: "user1",
domain: undefined,
social_connection: "github",
},
});
});
});
51 changes: 51 additions & 0 deletions src/telemetry/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as vscode from "vscode";
import { UserInfo } from "../clients/sidecar/models/UserInfo";
import { getTelemetryLogger } from "./telemetryLogger";
import * as Sentry from "@sentry/node";

/** Given authenticated session and/or userInfo, clean the data & send an Identify event to Segment via TelemetryLogger
* @param eventName - The event name to be sent to Segment as a follow up per docs: "follow the Identify call with a Track event that records what caused the user to be identified"
* @param userInfo - The UserInfo object from the Authentiation event
* @param session - The vscode.AuthenticationSession object from an existing session
* ```
* sendTelemetryIdentifyEvent({eventName: "Event That Triggered Identify", userInfo: { id: "123", ...} });"
* ```
*/
export function sendTelemetryIdentifyEvent({
eventName,
userInfo,
session,
}: {
eventName: string;
userInfo: UserInfo | undefined;
session: vscode.AuthenticationSession | undefined;
}) {
const id = userInfo?.id || session?.account.id;
const username = userInfo?.username || session?.account.label;
const social_connection = userInfo?.social_connection;
let domain: string | undefined;
if (username) {
// email is redacted by VSCode TelemetryLogger, but we extract domain for Confluent analytics use
const emailRegex = /@[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+/;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd of maybe just done if @ present, then split and grab [1] since we don't care about other properties and expect it to always be an email entry anyway.

Copy link
Member Author

Choose a reason for hiding this comment

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

does sound simpler! I wasn't sure if we were 100% certain this will be email every time. Happy to update if it makes sense to do so in follow ups

const match = username.match(emailRegex);
if (match) {
domain = username.split("@")[1];
}
}
if (id) {
getTelemetryLogger().logUsage(eventName, {
identify: true,
user: { id, domain, social_connection },
});
}
}

/** Helper function to make sure the user has Telemetry ON before sending Sentry error events */
export function checkTelemetrySettings(event: Sentry.Event) {
const telemetryLevel = vscode.workspace.getConfiguration()?.get("telemetry.telemetryLevel");
if (!vscode.env.isTelemetryEnabled || telemetryLevel === "off") {
// Returning `null` will drop the event
return null;
}
return event;
}
34 changes: 11 additions & 23 deletions src/telemetry.ts → src/telemetry/telemetryLogger.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Analytics } from "@segment/analytics-node";
import * as Sentry from "@sentry/node";
import { randomUUID } from "crypto";
import { version as currentSidecarVersion } from "ide-sidecar";
import * as vscode from "vscode";
import { Logger } from "./logging";
import { Logger } from "../logging";
// TEMP keep this import here to make sure the production bundle doesn't split chunks
import "opentelemetry-instrumentation-fetch-node";

Expand All @@ -30,12 +29,7 @@ let warnedAboutSegmentKey = false;
* Use Proper Case, Noun + Past Tense Verb to represent the user's action (e.g. "Order Completed", "File Downloaded", "User Registered")
* Optionally, add any relevant data as the second parameter
*
* For IDENTIFY calls - add the "identify" key to the data along with a user object (with at least an id) to send an identify type call.
* ```
* getTelemetryLogger().logUsage("Event That Triggered Identify", { identify: true, user: { id: "123", ...} });"
* ```
* It will send an Identify call followed by a Track event per this Segment recommendation:
* "Whenever possible, follow the Identify call with a Track event that records what caused the user to be identified."
* For IDENTIFY calls - use sendTelemetryIdentifyEvent from telemetry.ts instead
*/
export function getTelemetryLogger(): vscode.TelemetryLogger {
// If there is already an instance of the Segment Telemetry Logger, return it
Expand Down Expand Up @@ -65,25 +59,28 @@ export function getTelemetryLogger(): vscode.TelemetryLogger {
}
analytics = new Analytics({ writeKey, disable: false });
}
// We extract the vscode session ID from the event data, but this random id will be sent if it is undefined (unlikely but not guranteed by the type def)

segmentAnonId = randomUUID();

telemetryLogger = vscode.env.createTelemetryLogger({
sendEventData: (eventName, data) => {
const cleanEventName = eventName.replace(/^confluentinc\.vscode-confluent\//, ""); // Remove the prefix that vscode adds to event names
// Remove the prefix that vscode adds to event names
const cleanEventName = eventName.replace(/^confluentinc\.vscode-confluent\//, "");
// Extract & save the user id if was sent
if (data?.user?.id) userId = data.user.id;
if (data?.identify && userId) {
if (data?.identify && data?.user) {
analytics?.identify({
userId,
anonymousId: data?.["common.vscodesessionid"] || segmentAnonId,
traits: { email: data?.user?.username, social_connection: data?.user?.social_connection },
anonymousId: segmentAnonId,
traits: { ...data.user },
});
// We don't want to send the user traits or identify prop in the following Track call
delete data.identify;
delete data.user;
}
analytics?.track({
userId,
anonymousId: data?.["common.vscodesessionid"] || segmentAnonId,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why this change?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just setting us up to handle logout in near future... We're already sending vscodesessionid with the common properties in data, and the segmentAnonId is stable in each instance of the TelemetryLogger, so it won't make much difference now but in updates I'll be able to reset the anon id (during log out flow or other cases where the User Id might be new for an existing VSCode session)

anonymousId: segmentAnonId,
event: cleanEventName,
properties: { currentSidecarVersion, ...data }, // VSCode Common properties in data includes the extension version
});
Expand All @@ -98,12 +95,3 @@ export function getTelemetryLogger(): vscode.TelemetryLogger {

return telemetryLogger;
}

export function checkTelemetrySettings(event: Sentry.Event) {
const telemetryLevel = vscode.workspace.getConfiguration()?.get("telemetry.telemetryLevel");
if (!vscode.env.isTelemetryEnabled || telemetryLevel === "off") {
// Returning `null` will drop the event
return null;
}
return event;
}