Merge pull request #6810 from matrix-org/redact-all-rooms
Redact all room data in trackingpull/21833/head
commit
5c2467df7e
|
@ -29,10 +29,11 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
* - If [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is
|
* - If [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is
|
||||||
* enabled, events are not sent (this detection is built into posthog and turned on via the
|
* enabled, events are not sent (this detection is built into posthog and turned on via the
|
||||||
* `respect_dnt` flag being passed to `posthog.init`).
|
* `respect_dnt` flag being passed to `posthog.init`).
|
||||||
* - If the `feature_pseudonymous_analytics_opt_in` labs flag is `true`, track pseudonomously, i.e.
|
* - If the `feature_pseudonymous_analytics_opt_in` labs flag is `true`, track pseudonomously by maintaining
|
||||||
* hash all matrix identifiers in tracking events (user IDs, room IDs etc) using SHA-256.
|
* a randomised analytics ID in account_data for that user (shared between devices) and sending it to posthog to
|
||||||
* - Otherwise, if the existing `analyticsOptIn` flag is `true`, track anonymously, i.e.
|
identify the user.
|
||||||
* redact all matrix identifiers in tracking events.
|
* - Otherwise, if the existing `analyticsOptIn` flag is `true`, track anonymously, i.e. do not identify the user
|
||||||
|
using any identifier that would be consistent across devices.
|
||||||
* - If both flags are false or not set, events are not sent.
|
* - If both flags are false or not set, events are not sent.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -73,12 +74,6 @@ interface IPageView extends IAnonymousEvent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashHex = async (input: string): Promise<string> => {
|
|
||||||
const buf = new TextEncoder().encode(input);
|
|
||||||
const digestBuf = await window.crypto.subtle.digest("sha-256", buf);
|
|
||||||
return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const whitelistedScreens = new Set([
|
const whitelistedScreens = new Set([
|
||||||
"register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
|
"register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
|
||||||
"start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group",
|
"start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group",
|
||||||
|
@ -91,7 +86,6 @@ export async function getRedactedCurrentLocation(
|
||||||
anonymity: Anonymity,
|
anonymity: Anonymity,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// Redact PII from the current location.
|
// Redact PII from the current location.
|
||||||
// If anonymous is true, redact entirely, if false, substitute it with a hash.
|
|
||||||
// For known screens, assumes a URL structure of /<screen name>/might/be/pii
|
// For known screens, assumes a URL structure of /<screen name>/might/be/pii
|
||||||
if (origin.startsWith('file://')) {
|
if (origin.startsWith('file://')) {
|
||||||
pathname = "/<redacted_file_scheme_url>/";
|
pathname = "/<redacted_file_scheme_url>/";
|
||||||
|
@ -101,17 +95,13 @@ export async function getRedactedCurrentLocation(
|
||||||
if (hash == "") {
|
if (hash == "") {
|
||||||
hashStr = "";
|
hashStr = "";
|
||||||
} else {
|
} else {
|
||||||
let [beforeFirstSlash, screen, ...parts] = hash.split("/");
|
let [beforeFirstSlash, screen] = hash.split("/");
|
||||||
|
|
||||||
if (!whitelistedScreens.has(screen)) {
|
if (!whitelistedScreens.has(screen)) {
|
||||||
screen = "<redacted_screen_name>";
|
screen = "<redacted_screen_name>";
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
hashStr = `${beforeFirstSlash}/${screen}/<redacted>`;
|
||||||
parts[i] = anonymity === Anonymity.Anonymous ? `<redacted>` : await hashHex(parts[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
hashStr = `${beforeFirstSlash}/${screen}/${parts.join("/")}`;
|
|
||||||
}
|
}
|
||||||
return origin + pathname + hashStr;
|
return origin + pathname + hashStr;
|
||||||
}
|
}
|
||||||
|
@ -125,15 +115,15 @@ export class PosthogAnalytics {
|
||||||
/* Wrapper for Posthog analytics.
|
/* Wrapper for Posthog analytics.
|
||||||
* 3 modes of anonymity are supported, governed by this.anonymity
|
* 3 modes of anonymity are supported, governed by this.anonymity
|
||||||
* - Anonymity.Disabled means *no data* is passed to posthog
|
* - Anonymity.Disabled means *no data* is passed to posthog
|
||||||
* - Anonymity.Anonymous means all identifers will be redacted before being passed to posthog
|
* - Anonymity.Anonymous means no identifier is passed to posthog
|
||||||
* - Anonymity.Pseudonymous means all identifiers will be hashed via SHA-256 before being passed
|
* - Anonymity.Pseudonymous means an analytics ID stored in account_data and shared between devices
|
||||||
* to Posthog
|
* is passed to posthog.
|
||||||
*
|
*
|
||||||
* To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity().
|
* To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity().
|
||||||
*
|
*
|
||||||
* To pass an event to Posthog:
|
* To pass an event to Posthog:
|
||||||
*
|
*
|
||||||
* 1. Declare a type for the event, extending IAnonymousEvent, IPseudonymousEvent or IRoomEvent.
|
* 1. Declare a type for the event, extending IAnonymousEvent or IPseudonymousEvent.
|
||||||
* 2. Call the appropriate track*() method. Pseudonymous events will be dropped when anonymity is
|
* 2. Call the appropriate track*() method. Pseudonymous events will be dropped when anonymity is
|
||||||
* Anonymous or Disabled; Anonymous events will be dropped when anonymity is Disabled.
|
* Anonymous or Disabled; Anonymous events will be dropped when anonymity is Disabled.
|
||||||
*/
|
*/
|
||||||
|
@ -333,18 +323,6 @@ export class PosthogAnalytics {
|
||||||
await this.capture(eventName, properties);
|
await this.capture(eventName, properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async trackRoomEvent<E extends IRoomEvent>(
|
|
||||||
eventName: E["eventName"],
|
|
||||||
roomId: string,
|
|
||||||
properties: Omit<E["properties"], "roomId">,
|
|
||||||
): Promise<void> {
|
|
||||||
const updatedProperties = {
|
|
||||||
...properties,
|
|
||||||
hashedRoomId: roomId ? await hashHex(roomId) : null,
|
|
||||||
};
|
|
||||||
await this.trackPseudonymousEvent(eventName, updatedProperties);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async trackPageView(durationMs: number): Promise<void> {
|
public async trackPageView(durationMs: number): Promise<void> {
|
||||||
const hash = window.location.hash;
|
const hash = window.location.hash;
|
||||||
|
|
||||||
|
|
|
@ -136,18 +136,6 @@ describe("PosthogAnalytics", () => {
|
||||||
expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
|
expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should pass trackRoomEvent to posthog", async () => {
|
|
||||||
analytics.setAnonymity(Anonymity.Pseudonymous);
|
|
||||||
const roomId = "42";
|
|
||||||
await analytics.trackRoomEvent<IRoomEvent>("jest_test_event", roomId, {
|
|
||||||
foo: "bar",
|
|
||||||
});
|
|
||||||
expect(fakePosthog.capture.mock.calls[0][0]).toBe("jest_test_event");
|
|
||||||
expect(fakePosthog.capture.mock.calls[0][1]["foo"]).toEqual("bar");
|
|
||||||
expect(fakePosthog.capture.mock.calls[0][1]["hashedRoomId"])
|
|
||||||
.toEqual("73475cb40a568e8da8a045ced110137e159f890ac4da883b6b17dc651b3a8049");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should pass trackPseudonymousEvent() to posthog", async () => {
|
it("Should pass trackPseudonymousEvent() to posthog", async () => {
|
||||||
analytics.setAnonymity(Anonymity.Pseudonymous);
|
analytics.setAnonymity(Anonymity.Pseudonymous);
|
||||||
await analytics.trackPseudonymousEvent<ITestEvent>("jest_test_pseudo_event", {
|
await analytics.trackPseudonymousEvent<ITestEvent>("jest_test_pseudo_event", {
|
||||||
|
@ -173,9 +161,6 @@ describe("PosthogAnalytics", () => {
|
||||||
await analytics.trackAnonymousEvent<ITestEvent>("jest_test_event", {
|
await analytics.trackAnonymousEvent<ITestEvent>("jest_test_event", {
|
||||||
foo: "bar",
|
foo: "bar",
|
||||||
});
|
});
|
||||||
await analytics.trackRoomEvent<ITestRoomEvent>("room id", "foo", {
|
|
||||||
foo: "bar",
|
|
||||||
});
|
|
||||||
await analytics.trackPageView(200);
|
await analytics.trackPageView(200);
|
||||||
expect(fakePosthog.capture.mock.calls.length).toBe(0);
|
expect(fakePosthog.capture.mock.calls.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
@ -183,31 +168,25 @@ describe("PosthogAnalytics", () => {
|
||||||
it("Should pseudonymise a location of a known screen", async () => {
|
it("Should pseudonymise a location of a known screen", async () => {
|
||||||
const location = await getRedactedCurrentLocation(
|
const location = await getRedactedCurrentLocation(
|
||||||
"https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous);
|
"https://foo.bar", "#/register/some/pii", "/", Anonymity.Pseudonymous);
|
||||||
expect(location).toBe(
|
expect(location).toBe("https://foo.bar/#/register/<redacted>");
|
||||||
`https://foo.bar/#/register/\
|
|
||||||
a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\
|
|
||||||
bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should anonymise a location of a known screen", async () => {
|
it("Should anonymise a location of a known screen", async () => {
|
||||||
const location = await getRedactedCurrentLocation(
|
const location = await getRedactedCurrentLocation(
|
||||||
"https://foo.bar", "#/register/some/pii", "/", Anonymity.Anonymous);
|
"https://foo.bar", "#/register/some/pii", "/", Anonymity.Anonymous);
|
||||||
expect(location).toBe("https://foo.bar/#/register/<redacted>/<redacted>");
|
expect(location).toBe("https://foo.bar/#/register/<redacted>");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should pseudonymise a location of an unknown screen", async () => {
|
it("Should pseudonymise a location of an unknown screen", async () => {
|
||||||
const location = await getRedactedCurrentLocation(
|
const location = await getRedactedCurrentLocation(
|
||||||
"https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Pseudonymous);
|
"https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Pseudonymous);
|
||||||
expect(location).toBe(
|
expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>");
|
||||||
`https://foo.bar/#/<redacted_screen_name>/\
|
|
||||||
a6b46dd0d1ae5e86cbc8f37e75ceeb6760230c1ca4ffbcb0c97b96dd7d9c464b/\
|
|
||||||
bd75b3e080945674c0351f75e0db33d1e90986fa07b318ea7edf776f5eef38d4`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should anonymise a location of an unknown screen", async () => {
|
it("Should anonymise a location of an unknown screen", async () => {
|
||||||
const location = await getRedactedCurrentLocation(
|
const location = await getRedactedCurrentLocation(
|
||||||
"https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Anonymous);
|
"https://foo.bar", "#/not_a_screen_name/some/pii", "/", Anonymity.Anonymous);
|
||||||
expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>/<redacted>");
|
expect(location).toBe("https://foo.bar/#/<redacted_screen_name>/<redacted>");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Should handle an empty hash", async () => {
|
it("Should handle an empty hash", async () => {
|
||||||
|
|
Loading…
Reference in New Issue