Skip to content

Commit

Permalink
Merge pull request #2740 from tallyhowallet/analytics-tests
Browse files Browse the repository at this point in the history
🤖 ✅ Am I Real: Tests for analytics service
  • Loading branch information
jagodarybacka authored Jan 4, 2023
2 parents 340e3c0 + 0f16ca2 commit 9f709d1
Show file tree
Hide file tree
Showing 11 changed files with 362 additions and 53 deletions.
9 changes: 9 additions & 0 deletions __mocks__/uuid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const uuidMock = jest.createMockFromModule<typeof import("uuid")>("uuid")
const uuidActual = jest.requireActual<typeof import("uuid")>("uuid")

const v4Spy = jest.spyOn(uuidActual, "v4")

module.exports = {
...uuidMock,
v4: v4Spy,
}
33 changes: 33 additions & 0 deletions __mocks__/webextension-polyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Tabs } from "webextension-polyfill"

const browserMock = jest.createMockFromModule<
typeof import("webextension-polyfill")
>("webextension-polyfill")

module.exports = {
...browserMock,
alarms: {
create: () => {},
clear: () => {},
onAlarm: {
addListener: () => {},
removeListener: () => {},
},
},
extension: {
...browserMock.extension,
getBackgroundPage: jest.fn(),
},
tabs: {
...browserMock.tabs,
getCurrent: jest.fn(() =>
// getCurrent can return undefined if there is no tab, and we act accordingly
// in the code.
Promise.resolve(undefined as unknown as Tabs.Tab)
),
},
runtime: {
...browserMock.runtime,
setUninstallURL: jest.fn(),
},
}
10 changes: 2 additions & 8 deletions background/lib/posthog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,13 @@ import logger from "./logger"
export const POSTHOG_URL =
process.env.POSTHOG_URL ?? "https://app.posthog.com/capture/"

// Destructuring doesn't work with env variables. process.nev is `MISSING ENV VAR` in that case
// eslint-disable-next-line prefer-destructuring
export const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY

// Destructuring doesn't work with env variables. process.nev is `MISSING ENV VAR` in that case
// eslint-disable-next-line prefer-destructuring
export const USE_ANALYTICS_SOURCE = process.env.USE_ANALYTICS_SOURCE

export function shouldSendPosthogEvents(): boolean {
return (
isEnabled(FeatureFlags.SUPPORT_ANALYTICS) &&
!!POSTHOG_URL &&
!!POSTHOG_API_KEY
isEnabled(FeatureFlags.SUPPORT_ANALYTICS) && !!process.env.POSTHOG_API_KEY
)
}

Expand All @@ -34,7 +28,7 @@ export function createPosthogPayload(
// The unique or anonymous id of the user that triggered the event.
distinct_id: personUUID,
// api key
api_key: POSTHOG_API_KEY,
api_key: process.env.POSTHOG_API_KEY,
// name of the event
event: eventName,
// Let's include a timestamp just to be sure. Optional.
Expand Down
17 changes: 3 additions & 14 deletions background/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1532,21 +1532,10 @@ export default class Main extends BaseService<never> {
}

async connectAnalyticsService(): Promise<void> {
const { hasDefaultOnBeenTurnedOn } =
await this.preferenceService.getAnalyticsPreferences()

if (
isEnabled(FeatureFlags.ENABLE_ANALYTICS_DEFAULT_ON) &&
!hasDefaultOnBeenTurnedOn
) {
// TODO: Remove this in the next release after we switch on
// analytics by default
await this.preferenceService.updateAnalyticsPreferences({
isEnabled: true,
hasDefaultOnBeenTurnedOn: true,
})
this.analyticsService.emitter.on("enableDefaultOn", () => {
this.store.dispatch(setShowAnalyticsNotification(true))
}
})

this.preferenceService.emitter.on(
"updateAnalyticsPreferences",
async (analyticsPreferences: AnalyticsPreferences) => {
Expand Down
2 changes: 1 addition & 1 deletion background/redux-slices/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { AccountSignerSettings } from "../ui"
import { AccountState, addAddressNetwork } from "./accounts"
import { createBackgroundAsyncThunk } from "./utils"

const defaultSettings = {
export const defaultSettings = {
hideDust: false,
defaultWallet: false,
showTestNetworks: false,
Expand Down
20 changes: 11 additions & 9 deletions background/services/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import PreferenceService from "../preferences"
import { FeatureFlags, isEnabled as isFeatureFlagEnabled } from "../../features"

interface Events extends ServiceLifecycleEvents {
placeHolderEventForTypingPurposes: string
enableDefaultOn: void
}

/*
Expand Down Expand Up @@ -62,18 +62,24 @@ export default class AnalyticsService extends BaseService<Events> {
isEnabled,
hasDefaultOnBeenTurnedOn,
})

await this.emitter.emit("enableDefaultOn", undefined)
}

if (isEnabled) {
this.sendAnalyticsEvent("Background start")

this.initializeListeners()

const { uuid } = await this.getOrCreateAnalyticsUUID()
const { uuid, isNew } = await this.getOrCreateAnalyticsUUID()

browser.runtime.setUninstallURL(
`${process.env.WEBSITE_ORIGIN}/goodbye?uuid=${uuid}`
)

if (isNew) {
await this.sendAnalyticsEvent("New install")
}

await this.sendAnalyticsEvent("Background start")
}
}

Expand All @@ -91,11 +97,7 @@ export default class AnalyticsService extends BaseService<Events> {

const { isEnabled } = await this.preferenceService.getAnalyticsPreferences()
if (isEnabled) {
const { uuid, isNew } = await this.getOrCreateAnalyticsUUID()

if (isNew) {
sendPosthogEvent(uuid, "New install", payload)
}
const { uuid } = await this.getOrCreateAnalyticsUUID()

sendPosthogEvent(uuid, eventName, payload)
}
Expand Down
238 changes: 238 additions & 0 deletions background/services/analytics/tests/index.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// We use the classInstance["privateMethodOrVariableName"] to access private properties in a type safe way
// without redefining the types. This is a typescript shortcoming that we can't easily redefine class member visibility.
// https://github.com/microsoft/TypeScript/issues/22677
// POC https://www.typescriptlang.org/play?#code/MYGwhgzhAEBiD29oG8BQ0PWPAdhALgE4Cuw+8hAFAA6ECWAbmPgKbRgBc0B9OA5gBpotRszYAjLjmIBbcS0IBKFAF9Ua1CBb5oAM0TQAvNBwsA7nESUARGHHBrQgIwAmAMyLU2PPC0A6EHg+Sn14AG1bawBdZQB6WOgAeQBpVHisXAhfFgCgkMQIgH0waLiEgFEAJUrEyrSE0IiSqKNoAFZodKqayqA
/* eslint-disable @typescript-eslint/dot-notation */

import * as uuid from "uuid"
import browser from "webextension-polyfill"

import AnalyticsService from ".."
import * as features from "../../../features"
import { createAnalyticsService } from "../../../tests/factories"
import { Writeable } from "../../../types"
import PreferenceService from "../../preferences"
import * as posthog from "../../../lib/posthog"

describe("AnalyticsService", () => {
let analyticsService: AnalyticsService
let preferenceService: PreferenceService
const runtimeFlagWritable = features.RuntimeFlag as Writeable<
typeof features.RuntimeFlag
>
beforeAll(() => {
global.fetch = jest.fn()
// We need this set otherwise the posthog lib won't send the events
process.env.POSTHOG_API_KEY = "hey hey hey"
})
beforeEach(async () => {
jest.clearAllMocks()

analyticsService = await createAnalyticsService()
preferenceService = analyticsService["preferenceService"]
})
describe("the setup starts with the proper environment setup", () => {
it("PreferenceService should be initialized with isEnabled off and hasDefaultOnBeenTurnedOn off by default", async () => {
jest.spyOn(preferenceService, "getAnalyticsPreferences")

expect(await preferenceService.getAnalyticsPreferences()).toEqual({
isEnabled: false,
hasDefaultOnBeenTurnedOn: false,
})

expect(preferenceService.getAnalyticsPreferences).toBeCalled()
})
it("should change the isEnabled output based on the changed feature flag", () => {
runtimeFlagWritable.SUPPORT_ANALYTICS = false
runtimeFlagWritable.ENABLE_ANALYTICS_DEFAULT_ON = false

expect(features.isEnabled(features.FeatureFlags.SUPPORT_ANALYTICS)).toBe(
false
)
expect(
features.isEnabled(features.FeatureFlags.ENABLE_ANALYTICS_DEFAULT_ON)
).toBe(false)

runtimeFlagWritable.SUPPORT_ANALYTICS = true
runtimeFlagWritable.ENABLE_ANALYTICS_DEFAULT_ON = true

expect(features.isEnabled(features.FeatureFlags.SUPPORT_ANALYTICS)).toBe(
true
)
expect(
features.isEnabled(features.FeatureFlags.ENABLE_ANALYTICS_DEFAULT_ON)
).toBe(true)
})
})
describe("before the feature is released (the feature flags are off)", () => {
beforeEach(async () => {
jest.spyOn(analyticsService, "sendAnalyticsEvent")

runtimeFlagWritable.SUPPORT_ANALYTICS = false
runtimeFlagWritable.ENABLE_ANALYTICS_DEFAULT_ON = false

await analyticsService.startService()
})
it("should not send any analytics events when both of the feature flags are off", async () => {
await analyticsService.sendAnalyticsEvent("Background start")

expect(fetch).not.toBeCalled()
})
it("should not send any analytics events when only the support feature flag is on but the default on is not", async () => {
await analyticsService.sendAnalyticsEvent("Background start")

expect(fetch).not.toBeCalled()
})
})
describe("when the feature is released (feature flags are on, but settings is still off)", () => {
beforeEach(async () => {
jest.spyOn(analyticsService["db"], "setAnalyticsUUID")
jest.spyOn(analyticsService.emitter, "emit")
jest.spyOn(preferenceService, "updateAnalyticsPreferences")
jest.spyOn(preferenceService.emitter, "emit")
jest.spyOn(posthog, "sendPosthogEvent")

runtimeFlagWritable.SUPPORT_ANALYTICS = true
runtimeFlagWritable.ENABLE_ANALYTICS_DEFAULT_ON = true

await analyticsService.startService()
})

it("should change isEnabled and hasDefaultOnBeenTurnedOn to true in PreferenceService", async () => {
// The default off value for analytics settings in PreferenceService has a test in the environment setup describe
expect(await preferenceService.getAnalyticsPreferences()).toEqual({
isEnabled: true,
hasDefaultOnBeenTurnedOn: true,
})
expect(preferenceService.updateAnalyticsPreferences).toBeCalledTimes(1)
})
it("should emit enableDefaultOn and settings update event to notify UI", async () => {
expect(analyticsService.emitter.emit).toBeCalledTimes(2)
expect(analyticsService.emitter.emit).toHaveBeenCalledWith(
"enableDefaultOn",
undefined
)
expect(analyticsService.emitter.emit).toHaveBeenCalledWith(
"serviceStarted",
undefined
)

expect(preferenceService.emitter.emit).toBeCalledTimes(1)
expect(preferenceService.emitter.emit).toBeCalledWith(
"updateAnalyticsPreferences",
{
isEnabled: true,
hasDefaultOnBeenTurnedOn: true,
}
)
expect(preferenceService.updateAnalyticsPreferences).toBeCalledTimes(1)
})

it("should generate a new uuid and save it to database", async () => {
// Called once for generating the new user uuid
// and once for the 'Background start' and once for the `New install' event
expect(uuid.v4).toBeCalledTimes(3)

expect(analyticsService["db"].setAnalyticsUUID).toBeCalledTimes(1)
})

it("should send 'New Install' and 'Background start' events", () => {
// Posthog events are sent through global.fetch method
// During initialization we send 2 events
expect(fetch).toBeCalledTimes(2)

expect(posthog.sendPosthogEvent).toHaveBeenCalledWith(
expect.anything(),
"New install",
undefined
)
expect(posthog.sendPosthogEvent).toHaveBeenCalledWith(
expect.anything(),
"Background start",
undefined
)
})
})
describe("feature is released and enabled (analytics uuid has been created earlier)", () => {
beforeEach(async () => {
jest.spyOn(analyticsService, "sendAnalyticsEvent")
jest.spyOn(preferenceService, "updateAnalyticsPreferences")
jest
.spyOn(preferenceService, "getAnalyticsPreferences")
.mockImplementation(() =>
Promise.resolve({
isEnabled: true,
hasDefaultOnBeenTurnedOn: true,
})
)

runtimeFlagWritable.SUPPORT_ANALYTICS = true
runtimeFlagWritable.ENABLE_ANALYTICS_DEFAULT_ON = true

// Initialize analytics uuid
await analyticsService["getOrCreateAnalyticsUUID"]()

await analyticsService.startService()
})
it("should not run the initialization flow", async () => {
// uuid should be already present
expect(await analyticsService["getOrCreateAnalyticsUUID"]()).toEqual(
expect.objectContaining({ isNew: false })
)

expect(analyticsService.sendAnalyticsEvent).not.toHaveBeenCalledWith(
expect.anything(),
"New install",
undefined
)

expect(
preferenceService.updateAnalyticsPreferences
).not.toHaveBeenCalled()
})
it("should send 'Background start' event when the service starts", async () => {
expect(analyticsService.sendAnalyticsEvent).toBeCalledTimes(1)
expect(analyticsService.sendAnalyticsEvent).toBeCalledWith(
"Background start"
)
})
it("should set the uninstall url when the service starts", async () => {
expect(browser.runtime.setUninstallURL).toBeCalledTimes(1)
})
})
describe("feature is released but disabled", () => {
beforeEach(async () => {
jest.spyOn(analyticsService, "sendAnalyticsEvent")
jest.spyOn(posthog, "sendPosthogEvent")
jest
.spyOn(preferenceService, "getAnalyticsPreferences")
.mockImplementation(() =>
Promise.resolve({
isEnabled: false,
hasDefaultOnBeenTurnedOn: true,
})
)

runtimeFlagWritable.SUPPORT_ANALYTICS = true
runtimeFlagWritable.ENABLE_ANALYTICS_DEFAULT_ON = true

// Initialize analytics uuid
await analyticsService["getOrCreateAnalyticsUUID"]()

await analyticsService.startService()
})
it("should not send any event when the service starts", async () => {
expect(analyticsService.sendAnalyticsEvent).not.toBeCalled()

expect(posthog.sendPosthogEvent).not.toBeCalled()
expect(fetch).not.toBeCalled()
})
it("should not send any event when the 'sendAnalyticsEvent()' method is called", async () => {
await analyticsService.sendAnalyticsEvent("Background start")
expect(analyticsService.sendAnalyticsEvent).toBeCalledTimes(1)

expect(posthog.sendPosthogEvent).not.toBeCalled()
expect(fetch).not.toBeCalled()
})
})
})
Loading

0 comments on commit 9f709d1

Please sign in to comment.