Skip to content
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
3 changes: 3 additions & 0 deletions fission/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ dist-ssr
*.sw?

yarn.lock

test-results
src/test/**/__screenshots__
2 changes: 1 addition & 1 deletion fission/biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
"$schema": "https://biomejs.dev/schemas/2.1.3/schema.json",
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
"files": { "ignoreUnknown": false },
"formatter": {
Expand Down
1,685 changes: 863 additions & 822 deletions fission/bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions fission/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"colord": "^2.9.3",
"framer-motion": "^10.18.0",
"lygia": "^1.3.3",
"msw": "^2.10.4",
"notistack": "^3.0.2",
"playwright": "^1.54.2",
"postprocessing": "^6.37.6",
Expand Down
3 changes: 2 additions & 1 deletion fission/src/GA.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ declare module "@haensl/google-analytics" {
function event(e: GaEvent)
function exception(e: GaException)
function setUserId({ id }: { id: string })
function setUserProperty({ name, value }: { name: string; value: string })
function setUserProperty({ name, value }: { name: string; value: unknown })
function install()
}
3 changes: 2 additions & 1 deletion fission/src/Window.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
declare interface Window {
convertAuthToken(code: string): void
gtag: () => void
gtag?: (command: "config" | "set" | "get" | "event" | "consent", ...args: unknown[]) => void
dataLayer?: unknown[][]
}
10 changes: 9 additions & 1 deletion fission/src/mirabuf/MirabufLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ class MirabufCachingService {
const miraBuff = await resp.arrayBuffer()

World.analyticsSystem?.event("Remote Download", {
assemblyName: name ?? fetchLocation,
type: miraType === MiraType.ROBOT ? "robot" : "field",
fileSize: miraBuff.byteLength,
})
Expand Down Expand Up @@ -283,6 +284,13 @@ class MirabufCachingService {
}
}

World.analyticsSystem?.event("Local Upload", {
assemblyName: displayName,
fileSize: buffer.byteLength,
key,
type: miraType == MiraType.ROBOT ? "robot" : "field",
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you put a note in #1190 to fix this if this PR gets merged first.

})

if (!target) {
const cacheInfo = await MirabufCachingService.storeInCache(key, buffer, miraType, displayName)
if (cacheInfo) {
Expand Down Expand Up @@ -517,7 +525,7 @@ class MirabufCachingService {
window.localStorage.setItem(miraType == MiraType.ROBOT ? robotsDirName : fieldsDirName, JSON.stringify(map))

World.analyticsSystem?.event("Cache Store", {
name: name ?? "-",
assemblyName: name ?? "-",
key: key,
type: miraType == MiraType.ROBOT ? "robot" : "field",
fileSize: miraBuff.byteLength,
Expand Down
61 changes: 48 additions & 13 deletions fission/src/systems/analytics/AnalyticsSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const SAMPLE_INTERVAL = 60000 // 1 minute
const BETA_CODE_COOKIE_REGEX = /access_code=.*(;|$)/
const MOBILE_USER_AGENT_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i

declare const GIT_COMMIT: string

export interface AccumTimes {
frames: number
physicsTime: number
Expand All @@ -16,6 +18,42 @@ export interface AccumTimes {
simulationTime: number
totalTime: number
}
type MiraEvent = {
key?: string
type?: "robot" | "field"
assemblyName?: string
/**
* Size (in bytes) of the mirabuf file
*/
fileSize?: number
}
export interface AnalyticsEvents {
"Performance Sample": {
frames: number
avgTotal: number
avgPhysics: number
avgScene: number
avgInput: number
avgSimulation: number
}
"APS Calls per Minute": unknown
"APS Login": unknown
"APS Download": MiraEvent

"Cache Get": MiraEvent
"Cache Store": MiraEvent
"Cache Remove": MiraEvent

"Remote Download": MiraEvent
"Local Upload": MiraEvent

"Devtool Cache Persist": MiraEvent

"Scheme Applied": {
isCustomized: boolean
schemeName: string
}
}

class AnalyticsSystem extends WorldSystem {
private _lastSampleTime = Date.now()
Expand All @@ -38,7 +76,7 @@ class AnalyticsSystem extends WorldSystem {
this.sendMetaData()
}

public event(name: string, params?: { [key: string]: string | number }) {
public event<K extends keyof AnalyticsEvents>(name: K, params?: AnalyticsEvents[K]) {
event({ name: name, params: params ?? {} })
}

Expand All @@ -50,7 +88,11 @@ class AnalyticsSystem extends WorldSystem {
setUserId({ id: id })
}

public setUserProperty(name: string, value: string) {
public setUserProperty(name: string, value: unknown) {
if (name.includes(" ")) {
console.warn("GA user property names must not contain spaces")
return
}
setUserProperty({ name: name, value: value })
}

Expand All @@ -62,9 +104,8 @@ class AnalyticsSystem extends WorldSystem {
}

private sendMetaData() {
if (import.meta.env.DEV) {
this.setUserProperty("Internal Traffic", "true")
}
this.setUserProperty("isInternal", import.meta.env.DEV)
this.setUserProperty("commit", GIT_COMMIT)

if (!this._consent) {
return
Expand All @@ -73,15 +114,9 @@ class AnalyticsSystem extends WorldSystem {
let betaCode = document.cookie.match(BETA_CODE_COOKIE_REGEX)?.[0]
if (betaCode) {
betaCode = betaCode.substring(betaCode.indexOf("=") + 1, betaCode.indexOf(";"))

this.setUserProperty("Beta Code", betaCode)
}

if (MOBILE_USER_AGENT_REGEX.test(navigator.userAgent)) {
this.setUserProperty("Is Mobile", "true")
} else {
this.setUserProperty("Is Mobile", "false")
this.setUserProperty("betaCode", betaCode)
}
this.setUserProperty("isMobile", MOBILE_USER_AGENT_REGEX.test(navigator.userAgent))
}

private currentSampleInterval() {
Expand Down
2 changes: 1 addition & 1 deletion fission/src/systems/input/InputSchemeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class InputSchemeManager {
result[scheme.schemeName] ??= {
scheme,
status: InputSchemeUseType.CONFLICT,
conflicts_with_names: [...new Set(conflictingSchemes)].join(", "),
conflictingSchemeNames: [...new Set(conflictingSchemes)].join(", "),
}
} else {
result[scheme.schemeName] = {
Expand Down
9 changes: 9 additions & 0 deletions fission/src/systems/input/InputSystem.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { KeyCode } from "@/systems/input/KeyboardTypes.ts"
import { TouchControlsAxes } from "@/ui/components/TouchControls"
import Joystick from "../scene/Joystick"
import World from "../World"
import WorldSystem from "../WorldSystem"
import type { InputName, InputScheme, ModifierState } from "./InputTypes"
import type Input from "./inputs/Input"
Expand All @@ -26,6 +27,14 @@ class InputSystem extends WorldSystem {
/** Maps a brain index to an input scheme. */
public static brainIndexSchemeMap: Map<number, InputScheme> = new Map()

public static setBrainIndexSchemeMapping(index: number, scheme: InputScheme) {
InputSystem.brainIndexSchemeMap.set(index, scheme)
World.analyticsSystem?.event("Scheme Applied", {
isCustomized: scheme.customized,
schemeName: scheme.schemeName,
})
}

constructor() {
super()

Expand Down
5 changes: 3 additions & 2 deletions fission/src/systems/input/InputTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export enum InputSchemeUseType {
export type InputSchemeAvailability = {
scheme: InputScheme
status: InputSchemeUseType
conflicts_with_names?: string
conflictingSchemeNames?: string
}

export const EMPTY_MODIFIER_STATE: ModifierState = {
Expand All @@ -39,4 +39,5 @@ export const EMPTY_MODIFIER_STATE: ModifierState = {
meta: false,
}

export type KeyDescriptor = (string & { __: "" }) | null // prevent strings from being assigned without explicit casting
// biome-ignore lint/style/useNamingConvention: prevent strings from being assigned without explicit casting
export type KeyDescriptor = (string & { __: "KeyDescriptor" }) | null
1 change: 1 addition & 0 deletions fission/src/systems/match_mode/MatchMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class MatchMode {
this._matchModeType = val
new MatchStateChangeEvent(val).dispatch()
}

private _initialTime: number = 0
private _timeLeft: number = 0
private _intervalId: number | null = null
Expand Down
4 changes: 2 additions & 2 deletions fission/src/test/InputSystem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ describe("Input System Checks", () => {
})

test("Arcade Drive", () => {
InputSystem.brainIndexSchemeMap.set(0, DefaultInputs.ernie())
InputSystem.setBrainIndexSchemeMapping(0, DefaultInputs.ernie())
inputSystem.update(-1) // Initialize the input system

function testArcadeInput(inputMap: InputName, key: string, expectedValue: number) {
Expand Down Expand Up @@ -264,7 +264,7 @@ describe("Gamepad Input Check", () => {
const scheme = DefaultInputs.newBlankScheme(DriveType.ARCADE)
scheme.usesGamepad = true
scheme.inputs = [new ButtonInput("joint 4", undefined, 0)]
InputSystem.brainIndexSchemeMap.set(42, scheme)
InputSystem.setBrainIndexSchemeMapping(42, scheme)

vi.spyOn(InputSystem, "isGamepadButtonPressed").mockReturnValue(true)
expect(InputSystem.getInput("joint 4", 42)).toBe(1)
Expand Down
Empty file.
105 changes: 105 additions & 0 deletions fission/src/test/analytics/Analytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { install } from "@haensl/google-analytics"
import { server } from "@vitest/browser/context"
import { HttpResponse, http } from "msw"
import { setupWorker } from "msw/browser"
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, expectTypeOf, type Mock, test, vi } from "vitest"
import AnalyticsSystem from "@/systems/analytics/AnalyticsSystem.ts"
import PreferencesSystem from "@/systems/preferences/PreferencesSystem.ts"

type RequestType = Parameters<Parameters<typeof http.get>[1]>[0]
const tagID = "G-6XNCRD7QNC"

describe("Analytics", () => {
const gtagRequestMock: Mock<(req: RequestType) => void> = vi.fn(() => {})

const restHandlers = [
http.post("https://www.google-analytics.com/g/collect", req => {
gtagRequestMock(req)
return HttpResponse.text("")
}),
]

const webMocks = setupWorker(...restHandlers)

// Start server before all tests
beforeAll(async () => await webMocks.start({ onUnhandledRequest: "bypass", quiet: true }))

// Close server after all tests
afterAll(() => webMocks.stop())

// Reset handlers after each test `important for test isolation`
afterEach(() => webMocks.resetHandlers())
beforeEach(() => {
vi.resetAllMocks()
})

afterEach(() => {})

const mockRequestParametersHandle = () => {
return new Promise<URLSearchParams>(resolve => {
gtagRequestMock.mockImplementationOnce(req => {
resolve(new URL(req.request.url).searchParams)
})
})
}

describe("With gtag Script", () => {
beforeAll(async () => {
vi.useFakeTimers()
const script = document.createElement("script")
script.src = "https://www.googletagmanager.com/gtag/js?id=" + tagID
document.head.appendChild(script)
await vi.waitUntil(() => window.dataLayer != null, { timeout: 3000 })
install() // gtag is a function defined here to push to the datalayer object
})

test("google analytics loaded", async () => {
expect(window.gtag).toBeDefined()
expect(window.dataLayer).toBeDefined()
expectTypeOf(window.gtag!).toBeFunction()
expectTypeOf(window.dataLayer!).toBeArray()
})

test("gtag calls fetch with appropriate values", async ({ skip }) => {
skip(server.browser == "firefox", "Firefox blocks Google Analytics")
PreferencesSystem.setGlobalPreference("ReportAnalytics", true)

const initialParams = mockRequestParametersHandle()

const gtagSpy: Mock<NonNullable<typeof window.gtag>> = vi.spyOn(window, "gtag")
const system = new AnalyticsSystem()
expect(gtagSpy).toHaveBeenCalled()
await initialParams.then(params => {
expect(params.get("tid")).toBe(tagID)
})
gtagSpy.mockClear()

const eventParams = mockRequestParametersHandle()
system.event("APS Calls per Minute", {})

expect(gtagSpy).toHaveBeenCalled()
await eventParams.then(params => {
expect(params.get("tid")).toBe(tagID)
expect(params.get("en")).toBe("APS Calls per Minute")
})
}, 20000)
})

describe("Without gtag Script", () => {
beforeEach(() => {
window.dataLayer = undefined
window.gtag = undefined
install()
})

test("gtag propagates to dataLayer", () => {
const initialSize = window.dataLayer!.length
window.gtag!("event", "test", { a: 2 })
expect(window.dataLayer!.length).toBe(initialSize + 1)
const lastDatalayerItem = window.dataLayer![window.dataLayer!.length - 1]
expect(lastDatalayerItem[0]).toBe("event")
expect(lastDatalayerItem[1]).toBe("test")
expect(lastDatalayerItem[2]).toStrictEqual(expect.objectContaining({ a: 2 }))
})
})
})
9 changes: 2 additions & 7 deletions fission/src/test/bootstrap/AppMount.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,7 @@
})

test("Static stylesheets load", async () => {
for (let i = 0; i < 50; i++) {
await wait(200)
if (document.styleSheets.length >= 2) {
break
}
}
await vi.waitUntil(() => document.styleSheets.length >= 2, { timeout: 10000, interval: 200 })

expect(document.styleSheets.length).toBe(2)
const iterable = document.fonts.values()
Expand All @@ -63,15 +58,15 @@
expect(window.convertAuthToken).toBeDefined()
expectTypeOf(window.convertAuthToken).toBeFunction()
expect(window.gtag).toBeDefined()
expectTypeOf(window.gtag).toBeFunction()
expectTypeOf(window.gtag!).toBeFunction()
await annotate("expected global functions mount")

Check notice on line 62 in fission/src/test/bootstrap/AppMount.test.tsx

View workflow job for this annotation

GitHub Actions / Playwright Unit Tests

expected global functions mount

// assorted style rules from index.css
const style = window.getComputedStyle(document.body)
expect(style.overflow).toBe("hidden")
expect(style.overscrollBehavior).toBe("none")
expect(style.fontFamily.split(",")[0].trim()).toBe("Artifakt")
await annotate("index.css applied correctly")

Check notice on line 69 in fission/src/test/bootstrap/AppMount.test.tsx

View workflow job for this annotation

GitHub Actions / Playwright Unit Tests

index.css applied correctly

expect(renderMock).toHaveBeenCalledOnce()
assert(screen != null, "Screen was null")
Expand All @@ -81,7 +76,7 @@
const screenElement = screen.baseElement
expect(screenElement.querySelector("canvas")).toBeInTheDocument()
expect(screen.getByText("Singleplayer")).toBeInTheDocument()
await annotate("DOM successfully updated to include Synthesis components")

Check notice on line 79 in fission/src/test/bootstrap/AppMount.test.tsx

View workflow job for this annotation

GitHub Actions / Playwright Unit Tests

DOM successfully updated to include Synthesis components
const initWorldSpy = vi.spyOn(World, "initWorld")
// for some reason threejs canvas intercepts .click()
screen
Expand All @@ -89,15 +84,15 @@
.element()
.dispatchEvent(new PointerEvent("click", { bubbles: true }))
expect(initWorldSpy).toHaveBeenCalledOnce()
await annotate("Singleplayer Button calls initWorld")

Check notice on line 87 in fission/src/test/bootstrap/AppMount.test.tsx

View workflow job for this annotation

GitHub Actions / Playwright Unit Tests

Singleplayer Button calls initWorld

await wait(50)

await annotate("Initial Scene DOM", { contentType: "text/html", body: document.documentElement.outerHTML })

Check notice on line 91 in fission/src/test/bootstrap/AppMount.test.tsx

View workflow job for this annotation

GitHub Actions / Playwright Unit Tests

Initial Scene DOM

screen.unmount()

await annotate("Screen unmounted gracefully")

Check notice on line 95 in fission/src/test/bootstrap/AppMount.test.tsx

View workflow job for this annotation

GitHub Actions / Playwright Unit Tests

Screen unmounted gracefully
}, 20000)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const ChooseInputSchemePanel: React.FC<PanelImplProps<void, void>> = ({ panel })
)?.scheme

if (scheme) {
InputSystem.brainIndexSchemeMap.set(brainIndex, scheme)
InputSystem.setBrainIndexSchemeMapping(brainIndex, scheme)
}
if (scheme) setSelectedScheme(scheme)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const InitialConfigPanel: React.FC<PanelImplProps<void, void>> = ({ panel }) =>
)?.scheme

if (scheme) {
InputSystem.brainIndexSchemeMap.set(brainIndex, scheme)
InputSystem.setBrainIndexSchemeMapping(brainIndex, scheme)
setSelectedScheme(scheme)
}
}
Expand Down
Loading