Add more encryption properties to PostHog (#12415)
* report time to decrypt an event if it's slow * add more stats * fix some tests * keep original timestamp, and report non-visible decryption failures * add statistic for whether the user trusts their own device * revert local playwright docker changes * apply changes from review * also remove logout event handler on logout * apply changes from review * make eventDecrypted callback private
This commit is contained in:
parent
0c04ec1def
commit
eed8d1538b
5 changed files with 626 additions and 180 deletions
|
@ -14,20 +14,30 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
import { CryptoEvent, HttpApiEvent, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { decryptExistingEvent, mkDecryptionFailureMatrixEvent } from "matrix-js-sdk/src/testing";
|
||||
import { DecryptionFailureCode } from "matrix-js-sdk/src/crypto-api";
|
||||
import { CryptoApi, DecryptionFailureCode, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { DecryptionFailureTracker } from "../src/DecryptionFailureTracker";
|
||||
import { DecryptionFailureTracker, ErrorProperties } from "../src/DecryptionFailureTracker";
|
||||
import { stubClient } from "./test-utils";
|
||||
|
||||
async function createFailedDecryptionEvent(code?: DecryptionFailureCode) {
|
||||
async function createFailedDecryptionEvent(opts: { sender?: string; code?: DecryptionFailureCode } = {}) {
|
||||
return await mkDecryptionFailureMatrixEvent({
|
||||
roomId: "!room:id",
|
||||
sender: "@alice:example.com",
|
||||
code: code ?? DecryptionFailureCode.UNKNOWN_ERROR,
|
||||
sender: opts.sender ?? "@alice:example.com",
|
||||
code: opts.code ?? DecryptionFailureCode.UNKNOWN_ERROR,
|
||||
msg: ":(",
|
||||
});
|
||||
}
|
||||
|
||||
// wrap tracker.eventDecrypted so that we don't need to have so many `ts-ignore`s
|
||||
function eventDecrypted(tracker: DecryptionFailureTracker, e: MatrixEvent, nowTs: number): void {
|
||||
// @ts-ignore access to private member
|
||||
return tracker.eventDecrypted(e, nowTs);
|
||||
}
|
||||
|
||||
describe("DecryptionFailureTracker", function () {
|
||||
it("tracks a failed decryption for a visible event", async function () {
|
||||
const failedDecryptionEvent = await createFailedDecryptionEvent();
|
||||
|
@ -35,12 +45,12 @@ describe("DecryptionFailureTracker", function () {
|
|||
let count = 0;
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(total: number) => (count += total),
|
||||
() => count++,
|
||||
() => "UnknownError",
|
||||
);
|
||||
|
||||
tracker.addVisibleEvent(failedDecryptionEvent);
|
||||
tracker.eventDecrypted(failedDecryptionEvent);
|
||||
eventDecrypted(tracker, failedDecryptionEvent, Date.now());
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
|
@ -53,23 +63,23 @@ describe("DecryptionFailureTracker", function () {
|
|||
});
|
||||
|
||||
it("tracks a failed decryption with expected raw error for a visible event", async function () {
|
||||
const failedDecryptionEvent = await createFailedDecryptionEvent(
|
||||
DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
|
||||
);
|
||||
const failedDecryptionEvent = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
let reportedRawCode = "";
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(total: number, _errCode: string, rawCode: string) => {
|
||||
count += total;
|
||||
(_errCode: string, rawCode: string) => {
|
||||
count++;
|
||||
reportedRawCode = rawCode;
|
||||
},
|
||||
() => "UnknownError",
|
||||
);
|
||||
|
||||
tracker.addVisibleEvent(failedDecryptionEvent);
|
||||
tracker.eventDecrypted(failedDecryptionEvent);
|
||||
eventDecrypted(tracker, failedDecryptionEvent, Date.now());
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
|
@ -90,11 +100,11 @@ describe("DecryptionFailureTracker", function () {
|
|||
let count = 0;
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(total: number) => (count += total),
|
||||
() => count++,
|
||||
() => "UnknownError",
|
||||
);
|
||||
|
||||
tracker.eventDecrypted(failedDecryptionEvent);
|
||||
eventDecrypted(tracker, failedDecryptionEvent, Date.now());
|
||||
tracker.addVisibleEvent(failedDecryptionEvent);
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
|
@ -107,33 +117,52 @@ describe("DecryptionFailureTracker", function () {
|
|||
expect(count).not.toBe(0);
|
||||
});
|
||||
|
||||
it("does not track a failed decryption for an event that never becomes visible", async function () {
|
||||
const failedDecryptionEvent = await createFailedDecryptionEvent();
|
||||
|
||||
let count = 0;
|
||||
it("tracks visible vs. not visible events", async () => {
|
||||
const propertiesByErrorCode: Record<string, ErrorProperties> = {};
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(total: number) => (count += total),
|
||||
() => "UnknownError",
|
||||
(errorCode: string, rawError: string, properties: ErrorProperties) => {
|
||||
propertiesByErrorCode[errorCode] = properties;
|
||||
},
|
||||
(error: string) => error,
|
||||
);
|
||||
|
||||
tracker.eventDecrypted(failedDecryptionEvent);
|
||||
// use three different errors so that we can distinguish the reports
|
||||
const error1 = DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID;
|
||||
const error2 = DecryptionFailureCode.MEGOLM_BAD_ROOM;
|
||||
const error3 = DecryptionFailureCode.MEGOLM_MISSING_FIELDS;
|
||||
|
||||
// event that will be marked as visible before it's marked as undecryptable
|
||||
const markedVisibleFirst = await createFailedDecryptionEvent({ code: error1 });
|
||||
// event that will be marked as undecryptable before it's marked as visible
|
||||
const markedUndecryptableFirst = await createFailedDecryptionEvent({ code: error2 });
|
||||
// event that is never marked as visible
|
||||
const neverVisible = await createFailedDecryptionEvent({ code: error3 });
|
||||
|
||||
tracker.addVisibleEvent(markedVisibleFirst);
|
||||
|
||||
const now = Date.now();
|
||||
eventDecrypted(tracker, markedVisibleFirst, now);
|
||||
eventDecrypted(tracker, markedUndecryptableFirst, now);
|
||||
eventDecrypted(tracker, neverVisible, now);
|
||||
|
||||
tracker.addVisibleEvent(markedUndecryptableFirst);
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
|
||||
// Immediately track the newest failures
|
||||
tracker.trackFailures();
|
||||
|
||||
// should not track a failure for an event that never became visible
|
||||
expect(count).toBe(0);
|
||||
expect(propertiesByErrorCode[error1].wasVisibleToUser).toBe(true);
|
||||
expect(propertiesByErrorCode[error2].wasVisibleToUser).toBe(true);
|
||||
expect(propertiesByErrorCode[error3].wasVisibleToUser).toBe(false);
|
||||
});
|
||||
|
||||
it("does not track a failed decryption where the event is subsequently successfully decrypted", async () => {
|
||||
const decryptedEvent = await createFailedDecryptionEvent();
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(_total: number) => {
|
||||
() => {
|
||||
// should not track an event that has since been decrypted correctly
|
||||
expect(true).toBe(false);
|
||||
},
|
||||
|
@ -141,14 +170,14 @@ describe("DecryptionFailureTracker", function () {
|
|||
);
|
||||
|
||||
tracker.addVisibleEvent(decryptedEvent);
|
||||
tracker.eventDecrypted(decryptedEvent);
|
||||
eventDecrypted(tracker, decryptedEvent, Date.now());
|
||||
|
||||
// Indicate successful decryption.
|
||||
await decryptExistingEvent(decryptedEvent, {
|
||||
plainType: "m.room.message",
|
||||
plainContent: { body: "success" },
|
||||
});
|
||||
tracker.eventDecrypted(decryptedEvent, null);
|
||||
eventDecrypted(tracker, decryptedEvent, Date.now());
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
|
@ -164,21 +193,21 @@ describe("DecryptionFailureTracker", function () {
|
|||
const decryptedEvent = await createFailedDecryptionEvent();
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(_total: number) => {
|
||||
() => {
|
||||
// should not track an event that has since been decrypted correctly
|
||||
expect(true).toBe(false);
|
||||
},
|
||||
() => "UnknownError",
|
||||
);
|
||||
|
||||
tracker.eventDecrypted(decryptedEvent);
|
||||
eventDecrypted(tracker, decryptedEvent, Date.now());
|
||||
|
||||
// Indicate successful decryption.
|
||||
await decryptExistingEvent(decryptedEvent, {
|
||||
plainType: "m.room.message",
|
||||
plainContent: { body: "success" },
|
||||
});
|
||||
tracker.eventDecrypted(decryptedEvent);
|
||||
eventDecrypted(tracker, decryptedEvent, Date.now());
|
||||
|
||||
tracker.addVisibleEvent(decryptedEvent);
|
||||
|
||||
|
@ -197,22 +226,23 @@ describe("DecryptionFailureTracker", function () {
|
|||
let count = 0;
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(total: number) => (count += total),
|
||||
() => count++,
|
||||
() => "UnknownError",
|
||||
);
|
||||
|
||||
tracker.addVisibleEvent(decryptedEvent);
|
||||
|
||||
// Arbitrary number of failed decryptions for both events
|
||||
tracker.eventDecrypted(decryptedEvent);
|
||||
tracker.eventDecrypted(decryptedEvent);
|
||||
tracker.eventDecrypted(decryptedEvent);
|
||||
tracker.eventDecrypted(decryptedEvent);
|
||||
tracker.eventDecrypted(decryptedEvent);
|
||||
tracker.eventDecrypted(decryptedEvent2);
|
||||
tracker.eventDecrypted(decryptedEvent2);
|
||||
const now = Date.now();
|
||||
eventDecrypted(tracker, decryptedEvent, now);
|
||||
eventDecrypted(tracker, decryptedEvent, now);
|
||||
eventDecrypted(tracker, decryptedEvent, now);
|
||||
eventDecrypted(tracker, decryptedEvent, now);
|
||||
eventDecrypted(tracker, decryptedEvent, now);
|
||||
eventDecrypted(tracker, decryptedEvent2, now);
|
||||
eventDecrypted(tracker, decryptedEvent2, now);
|
||||
tracker.addVisibleEvent(decryptedEvent2);
|
||||
tracker.eventDecrypted(decryptedEvent2);
|
||||
eventDecrypted(tracker, decryptedEvent2, now);
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
|
@ -233,14 +263,14 @@ describe("DecryptionFailureTracker", function () {
|
|||
let count = 0;
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(total: number) => (count += total),
|
||||
() => count++,
|
||||
() => "UnknownError",
|
||||
);
|
||||
|
||||
tracker.addVisibleEvent(decryptedEvent);
|
||||
|
||||
// Indicate decryption
|
||||
tracker.eventDecrypted(decryptedEvent);
|
||||
eventDecrypted(tracker, decryptedEvent, Date.now());
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
|
@ -248,7 +278,7 @@ describe("DecryptionFailureTracker", function () {
|
|||
tracker.trackFailures();
|
||||
|
||||
// Indicate a second decryption, after having tracked the failure
|
||||
tracker.eventDecrypted(decryptedEvent);
|
||||
eventDecrypted(tracker, decryptedEvent, Date.now());
|
||||
|
||||
tracker.trackFailures();
|
||||
|
||||
|
@ -265,14 +295,14 @@ describe("DecryptionFailureTracker", function () {
|
|||
let count = 0;
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(total: number) => (count += total),
|
||||
() => count++,
|
||||
() => "UnknownError",
|
||||
);
|
||||
|
||||
tracker.addVisibleEvent(decryptedEvent);
|
||||
|
||||
// Indicate decryption
|
||||
tracker.eventDecrypted(decryptedEvent);
|
||||
eventDecrypted(tracker, decryptedEvent, Date.now());
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
// NB: This saves to localStorage specific to DFT
|
||||
|
@ -291,7 +321,7 @@ describe("DecryptionFailureTracker", function () {
|
|||
|
||||
//secondTracker.loadTrackedEvents();
|
||||
|
||||
secondTracker.eventDecrypted(decryptedEvent);
|
||||
eventDecrypted(secondTracker, decryptedEvent, Date.now());
|
||||
secondTracker.checkFailures(Infinity);
|
||||
secondTracker.trackFailures();
|
||||
|
||||
|
@ -304,28 +334,31 @@ describe("DecryptionFailureTracker", function () {
|
|||
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(total: number, errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + total),
|
||||
(errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + 1),
|
||||
(error: DecryptionFailureCode) =>
|
||||
error === DecryptionFailureCode.UNKNOWN_ERROR ? "UnknownError" : "OlmKeysNotSentError",
|
||||
);
|
||||
|
||||
const decryptedEvent1 = await createFailedDecryptionEvent(DecryptionFailureCode.UNKNOWN_ERROR);
|
||||
const decryptedEvent2 = await createFailedDecryptionEvent(
|
||||
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
);
|
||||
const decryptedEvent3 = await createFailedDecryptionEvent(
|
||||
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
);
|
||||
const decryptedEvent1 = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.UNKNOWN_ERROR,
|
||||
});
|
||||
const decryptedEvent2 = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
});
|
||||
const decryptedEvent3 = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
});
|
||||
|
||||
tracker.addVisibleEvent(decryptedEvent1);
|
||||
tracker.addVisibleEvent(decryptedEvent2);
|
||||
tracker.addVisibleEvent(decryptedEvent3);
|
||||
|
||||
// One failure of ERROR_CODE_1, and effectively two for ERROR_CODE_2
|
||||
tracker.eventDecrypted(decryptedEvent1);
|
||||
tracker.eventDecrypted(decryptedEvent2);
|
||||
tracker.eventDecrypted(decryptedEvent2);
|
||||
tracker.eventDecrypted(decryptedEvent3);
|
||||
// One failure of UNKNOWN_ERROR, and effectively two for MEGOLM_UNKNOWN_INBOUND_SESSION_ID
|
||||
const now = Date.now();
|
||||
eventDecrypted(tracker, decryptedEvent1, now);
|
||||
eventDecrypted(tracker, decryptedEvent2, now);
|
||||
eventDecrypted(tracker, decryptedEvent2, now);
|
||||
eventDecrypted(tracker, decryptedEvent3, now);
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
|
@ -341,23 +374,28 @@ describe("DecryptionFailureTracker", function () {
|
|||
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(total: number, errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + total),
|
||||
(errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + 1),
|
||||
(_errorCode: string) => "OlmUnspecifiedError",
|
||||
);
|
||||
|
||||
const decryptedEvent1 = await createFailedDecryptionEvent(
|
||||
DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
);
|
||||
const decryptedEvent2 = await createFailedDecryptionEvent(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
|
||||
const decryptedEvent3 = await createFailedDecryptionEvent(DecryptionFailureCode.UNKNOWN_ERROR);
|
||||
const decryptedEvent1 = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
});
|
||||
const decryptedEvent2 = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
|
||||
});
|
||||
const decryptedEvent3 = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.UNKNOWN_ERROR,
|
||||
});
|
||||
|
||||
tracker.addVisibleEvent(decryptedEvent1);
|
||||
tracker.addVisibleEvent(decryptedEvent2);
|
||||
tracker.addVisibleEvent(decryptedEvent3);
|
||||
|
||||
tracker.eventDecrypted(decryptedEvent1);
|
||||
tracker.eventDecrypted(decryptedEvent2);
|
||||
tracker.eventDecrypted(decryptedEvent3);
|
||||
const now = Date.now();
|
||||
eventDecrypted(tracker, decryptedEvent1, now);
|
||||
eventDecrypted(tracker, decryptedEvent2, now);
|
||||
eventDecrypted(tracker, decryptedEvent3, now);
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
|
@ -372,13 +410,15 @@ describe("DecryptionFailureTracker", function () {
|
|||
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(total: number, errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + total),
|
||||
(errorCode: string) => (counts[errorCode] = (counts[errorCode] || 0) + 1),
|
||||
(errorCode: string) => Array.from(errorCode).reverse().join(""),
|
||||
);
|
||||
|
||||
const decryptedEvent = await createFailedDecryptionEvent(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
|
||||
const decryptedEvent = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
|
||||
});
|
||||
tracker.addVisibleEvent(decryptedEvent);
|
||||
tracker.eventDecrypted(decryptedEvent);
|
||||
eventDecrypted(tracker, decryptedEvent, Date.now());
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
|
@ -394,40 +434,56 @@ describe("DecryptionFailureTracker", function () {
|
|||
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(total: number, errorCode: string) => {
|
||||
(errorCode: string) => {
|
||||
errorCodes.push(errorCode);
|
||||
},
|
||||
// @ts-ignore access to private member
|
||||
DecryptionFailureTracker.instance.errorCodeMapFn,
|
||||
);
|
||||
|
||||
const event1 = await createFailedDecryptionEvent(DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID);
|
||||
const now = Date.now();
|
||||
|
||||
const event1 = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID,
|
||||
});
|
||||
tracker.addVisibleEvent(event1);
|
||||
tracker.eventDecrypted(event1);
|
||||
eventDecrypted(tracker, event1, now);
|
||||
|
||||
const event2 = await createFailedDecryptionEvent(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
|
||||
const event2 = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX,
|
||||
});
|
||||
tracker.addVisibleEvent(event2);
|
||||
tracker.eventDecrypted(event2);
|
||||
eventDecrypted(tracker, event2, now);
|
||||
|
||||
const event3 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
|
||||
const event3 = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP,
|
||||
});
|
||||
tracker.addVisibleEvent(event3);
|
||||
tracker.eventDecrypted(event3);
|
||||
eventDecrypted(tracker, event3, now);
|
||||
|
||||
const event4 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED);
|
||||
const event4 = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
|
||||
});
|
||||
tracker.addVisibleEvent(event4);
|
||||
tracker.eventDecrypted(event4);
|
||||
eventDecrypted(tracker, event4, now);
|
||||
|
||||
const event5 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP);
|
||||
const event5 = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP,
|
||||
});
|
||||
tracker.addVisibleEvent(event5);
|
||||
tracker.eventDecrypted(event5);
|
||||
eventDecrypted(tracker, event5, now);
|
||||
|
||||
const event6 = await createFailedDecryptionEvent(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED);
|
||||
const event6 = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED,
|
||||
});
|
||||
tracker.addVisibleEvent(event6);
|
||||
tracker.eventDecrypted(event6);
|
||||
eventDecrypted(tracker, event6, now);
|
||||
|
||||
const event7 = await createFailedDecryptionEvent(DecryptionFailureCode.UNKNOWN_ERROR);
|
||||
const event7 = await createFailedDecryptionEvent({
|
||||
code: DecryptionFailureCode.UNKNOWN_ERROR,
|
||||
});
|
||||
tracker.addVisibleEvent(event7);
|
||||
tracker.eventDecrypted(event7);
|
||||
eventDecrypted(tracker, event7, now);
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
|
@ -444,4 +500,217 @@ describe("DecryptionFailureTracker", function () {
|
|||
"UnknownError",
|
||||
]);
|
||||
});
|
||||
|
||||
it("tracks late decryptions vs. undecryptable", async () => {
|
||||
const propertiesByErrorCode: Record<string, ErrorProperties> = {};
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(errorCode: string, rawError: string, properties: ErrorProperties) => {
|
||||
propertiesByErrorCode[errorCode] = properties;
|
||||
},
|
||||
(error: string) => error,
|
||||
);
|
||||
|
||||
// use three different errors so that we can distinguish the reports
|
||||
const error1 = DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID;
|
||||
const error2 = DecryptionFailureCode.MEGOLM_BAD_ROOM;
|
||||
const error3 = DecryptionFailureCode.MEGOLM_MISSING_FIELDS;
|
||||
|
||||
// event that will be slow to decrypt
|
||||
const lateDecryption = await createFailedDecryptionEvent({ code: error1 });
|
||||
// event that will be so slow to decrypt, it gets counted as undecryptable
|
||||
const veryLateDecryption = await createFailedDecryptionEvent({ code: error2 });
|
||||
// event that never gets decrypted
|
||||
const neverDecrypted = await createFailedDecryptionEvent({ code: error3 });
|
||||
|
||||
tracker.addVisibleEvent(lateDecryption);
|
||||
tracker.addVisibleEvent(veryLateDecryption);
|
||||
tracker.addVisibleEvent(neverDecrypted);
|
||||
|
||||
const now = Date.now();
|
||||
eventDecrypted(tracker, lateDecryption, now);
|
||||
eventDecrypted(tracker, veryLateDecryption, now);
|
||||
eventDecrypted(tracker, neverDecrypted, now);
|
||||
|
||||
await decryptExistingEvent(lateDecryption, {
|
||||
plainType: "m.room.message",
|
||||
plainContent: { body: "success" },
|
||||
});
|
||||
await decryptExistingEvent(veryLateDecryption, {
|
||||
plainType: "m.room.message",
|
||||
plainContent: { body: "success" },
|
||||
});
|
||||
eventDecrypted(tracker, lateDecryption, now + 40000);
|
||||
eventDecrypted(tracker, veryLateDecryption, now + 100000);
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
|
||||
tracker.trackFailures();
|
||||
|
||||
expect(propertiesByErrorCode[error1].timeToDecryptMillis).toEqual(40000);
|
||||
expect(propertiesByErrorCode[error2].timeToDecryptMillis).toEqual(-1);
|
||||
expect(propertiesByErrorCode[error3].timeToDecryptMillis).toEqual(-1);
|
||||
});
|
||||
|
||||
it("listens for client events", async () => {
|
||||
// Test that the decryption failure tracker registers the right event
|
||||
// handlers on start, and unregisters them when the client logs out.
|
||||
const client = mocked(stubClient());
|
||||
const mockCrypto = {
|
||||
getVersion: jest.fn().mockReturnValue("Rust SDK 0.7.0 (61b175b), Vodozemac 0.5.1"),
|
||||
getUserVerificationStatus: jest.fn().mockResolvedValue(new UserVerificationStatus(false, false, false)),
|
||||
} as unknown as Mocked<CryptoApi>;
|
||||
client.getCrypto.mockReturnValue(mockCrypto);
|
||||
|
||||
let errorCount: number = 0;
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(errorCode: string, rawError: string, properties: ErrorProperties) => {
|
||||
errorCount++;
|
||||
},
|
||||
(error: string) => error,
|
||||
);
|
||||
|
||||
// Calling .start will start some intervals. This test shouldn't run
|
||||
// long enough for the timers to fire, but we'll use fake timers just
|
||||
// to be safe.
|
||||
jest.useFakeTimers();
|
||||
tracker.start(client);
|
||||
|
||||
// If the client fails to decrypt, it should get tracked
|
||||
const failedDecryption = await createFailedDecryptionEvent();
|
||||
client.emit(MatrixEventEvent.Decrypted, failedDecryption);
|
||||
|
||||
tracker.checkFailures(Infinity);
|
||||
tracker.trackFailures();
|
||||
|
||||
expect(errorCount).toEqual(1);
|
||||
|
||||
client.emit(HttpApiEvent.SessionLoggedOut, {} as any);
|
||||
|
||||
// After the client has logged out, we shouldn't be listening to events
|
||||
// any more, so even if the client emits an event regarding a failed
|
||||
// decryption, we won't track it.
|
||||
const anotherFailedDecryption = await createFailedDecryptionEvent();
|
||||
client.emit(MatrixEventEvent.Decrypted, anotherFailedDecryption);
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
tracker.trackFailures();
|
||||
|
||||
expect(errorCount).toEqual(1);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("tracks client information", async () => {
|
||||
const client = mocked(stubClient());
|
||||
const mockCrypto = {
|
||||
getVersion: jest.fn().mockReturnValue("Rust SDK 0.7.0 (61b175b), Vodozemac 0.5.1"),
|
||||
getUserVerificationStatus: jest.fn().mockResolvedValue(new UserVerificationStatus(false, false, false)),
|
||||
} as unknown as Mocked<CryptoApi>;
|
||||
client.getCrypto.mockReturnValue(mockCrypto);
|
||||
|
||||
const propertiesByErrorCode: Record<string, ErrorProperties> = {};
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(errorCode: string, rawError: string, properties: ErrorProperties) => {
|
||||
propertiesByErrorCode[errorCode] = properties;
|
||||
},
|
||||
(error: string) => error,
|
||||
);
|
||||
|
||||
// @ts-ignore access to private method
|
||||
await tracker.calculateClientProperties(client);
|
||||
// @ts-ignore access to private method
|
||||
await tracker.registerHandlers(client);
|
||||
|
||||
// use three different errors so that we can distinguish the reports
|
||||
const error1 = DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID;
|
||||
const error2 = DecryptionFailureCode.MEGOLM_BAD_ROOM;
|
||||
const error3 = DecryptionFailureCode.MEGOLM_MISSING_FIELDS;
|
||||
|
||||
// event from a federated user (@alice:example.com)
|
||||
const federatedDecryption = await createFailedDecryptionEvent({
|
||||
code: error1,
|
||||
});
|
||||
// event from a local user
|
||||
const localDecryption = await createFailedDecryptionEvent({
|
||||
sender: "@bob:matrix.org",
|
||||
code: error2,
|
||||
});
|
||||
|
||||
tracker.addVisibleEvent(federatedDecryption);
|
||||
tracker.addVisibleEvent(localDecryption);
|
||||
|
||||
const now = Date.now();
|
||||
eventDecrypted(tracker, federatedDecryption, now);
|
||||
|
||||
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(true, true, false));
|
||||
client.emit(CryptoEvent.KeysChanged, {});
|
||||
await sleep(100);
|
||||
eventDecrypted(tracker, localDecryption, now);
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
|
||||
tracker.trackFailures();
|
||||
|
||||
expect(propertiesByErrorCode[error1].isMatrixDotOrg).toBe(true);
|
||||
expect(propertiesByErrorCode[error1].cryptoSDK).toEqual("Rust");
|
||||
|
||||
expect(propertiesByErrorCode[error1].isFederated).toBe(true);
|
||||
expect(propertiesByErrorCode[error1].userTrustsOwnIdentity).toEqual(false);
|
||||
expect(propertiesByErrorCode[error2].isFederated).toBe(false);
|
||||
expect(propertiesByErrorCode[error2].userTrustsOwnIdentity).toEqual(true);
|
||||
|
||||
// change client params, and make sure the reports the right values
|
||||
client.getDomain.mockReturnValue("example.com");
|
||||
mockCrypto.getVersion.mockReturnValue("Olm 0.0.0");
|
||||
// @ts-ignore access to private method
|
||||
await tracker.calculateClientProperties(client);
|
||||
|
||||
const anotherFailure = await createFailedDecryptionEvent({
|
||||
code: error3,
|
||||
});
|
||||
tracker.addVisibleEvent(anotherFailure);
|
||||
eventDecrypted(tracker, anotherFailure, now);
|
||||
tracker.checkFailures(Infinity);
|
||||
tracker.trackFailures();
|
||||
expect(propertiesByErrorCode[error3].isMatrixDotOrg).toBe(false);
|
||||
expect(propertiesByErrorCode[error3].cryptoSDK).toEqual("Legacy");
|
||||
});
|
||||
|
||||
it("keeps the original timestamp after repeated decryption failures", async () => {
|
||||
const failedDecryptionEvent = await createFailedDecryptionEvent();
|
||||
|
||||
let failure: ErrorProperties | undefined;
|
||||
// @ts-ignore access to private constructor
|
||||
const tracker = new DecryptionFailureTracker(
|
||||
(errorCode: string, rawError: string, properties: ErrorProperties) => {
|
||||
failure = properties;
|
||||
},
|
||||
() => "UnknownError",
|
||||
);
|
||||
|
||||
tracker.addVisibleEvent(failedDecryptionEvent);
|
||||
|
||||
const now = Date.now();
|
||||
eventDecrypted(tracker, failedDecryptionEvent, now);
|
||||
eventDecrypted(tracker, failedDecryptionEvent, now + 20000);
|
||||
await decryptExistingEvent(failedDecryptionEvent, {
|
||||
plainType: "m.room.message",
|
||||
plainContent: { body: "success" },
|
||||
});
|
||||
eventDecrypted(tracker, failedDecryptionEvent, now + 50000);
|
||||
|
||||
// Pretend "now" is Infinity
|
||||
tracker.checkFailures(Infinity);
|
||||
tracker.trackFailures();
|
||||
|
||||
// the time to decrypt should be relative to the first time we failed
|
||||
// to decrypt, not the second
|
||||
expect(failure?.timeToDecryptMillis).toEqual(50000);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -955,6 +955,7 @@ describe("<MatrixChat />", () => {
|
|||
describe("post login setup", () => {
|
||||
beforeEach(() => {
|
||||
const mockCrypto = {
|
||||
getVersion: jest.fn().mockReturnValue("Version 0"),
|
||||
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
|
||||
getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()),
|
||||
getUserVerificationStatus: jest
|
||||
|
|
|
@ -98,6 +98,7 @@ export const unmockClientPeg = () => {
|
|||
*/
|
||||
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getDomain: jest.fn().mockReturnValue(userId.split(":")[1]),
|
||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||
getUser: jest.fn().mockReturnValue(new User(userId)),
|
||||
isGuest: jest.fn().mockReturnValue(false),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue