diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index be092be8b8..0abedbe7fc 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -18,7 +18,7 @@ export class ImmutableConfiguration { constructor(options: { environment: Environment }) { this.environment = options.environment; setEnvironment(options.environment); - track('config', 'load_imtbl_config'); + track('config', 'created_imtbl_config'); } } diff --git a/packages/game-bridge/.eslintrc b/packages/game-bridge/.eslintrc new file mode 100644 index 0000000000..f90c594b06 --- /dev/null +++ b/packages/game-bridge/.eslintrc @@ -0,0 +1,8 @@ +{ + "extends": ["../../.eslintrc"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json", + "tsconfigRootDir": "." + } +} diff --git a/packages/game-bridge/package.json b/packages/game-bridge/package.json index dcfd91587c..692bfef8b7 100644 --- a/packages/game-bridge/package.json +++ b/packages/game-bridge/package.json @@ -3,17 +3,20 @@ "version": "0.0.0", "dependencies": { "@imtbl/config": "0.0.0", + "@imtbl/metrics": "0.0.0", "@imtbl/passport": "0.0.0", "@imtbl/version-check": "0.0.0", "@imtbl/x-client": "0.0.0", "@imtbl/x-provider": "0.0.0" }, "devDependencies": { + "eslint": "^8.40.0", "parcel": "^2.8.3" }, "scripts": { "build": "yarn build:sdk && parcel build --no-cache --no-scope-hoist && yarn updateSdkVersion", "build:sdk": "cd ../.. && yarn build", + "lint": "eslint ./src --ext .ts,.jsx,.tsx --max-warnings=0", "start": "parcel", "updateSdkVersion": "./scripts/updateSdkVersion.sh" }, diff --git a/packages/game-bridge/src/index.ts b/packages/game-bridge/src/index.ts index b558a032ac..be30d1867a 100644 --- a/packages/game-bridge/src/index.ts +++ b/packages/game-bridge/src/index.ts @@ -2,6 +2,7 @@ import * as passport from '@imtbl/passport'; import * as config from '@imtbl/config'; import * as provider from '@imtbl/x-provider'; +import { track, identify } from '@imtbl/metrics'; import { gameBridgeVersionCheck } from '@imtbl/version-check'; /* eslint-disable no-undef */ @@ -13,6 +14,8 @@ const keyFunctionName = 'fxName'; const keyRequestId = 'requestId'; const keyData = 'data'; +const moduleName = 'game_bridge'; + // version check placeholders // eslint-disable-next-line @typescript-eslint/no-unused-vars const sdkVersionTag = '__SDK_VERSION__'; @@ -58,10 +61,10 @@ let zkEvmProviderInstance: passport.Provider | null; declare global { interface Window { - callFunction: (jsonData: string) => void, - ue: any, + callFunction: (jsonData: string) => void; + ue: any; // eslint-disable-next-line @typescript-eslint/naming-convention - Unity: any, + Unity: any; } } @@ -87,11 +90,15 @@ const callbackToGame = (data: object) => { } else if (window.Unity !== 'undefined') { window.Unity.call(message); } else { - console.error('No available game callbacks to call from ImmutableSDK game-bridge'); + console.error( + 'No available game callbacks to call from ImmutableSDK game-bridge', + ); } }; -const setProvider = (passportProvider: provider.IMXProvider | null): boolean => { +const setProvider = ( + passportProvider: provider.IMXProvider | null, +): boolean => { if (passportProvider !== null && passportProvider !== undefined) { providerInstance = passportProvider; console.log('IMX provider set'); @@ -125,7 +132,12 @@ const getZkEvmProvider = (): passport.Provider => { return zkEvmProviderInstance; }; -window.callFunction = async (jsonData: string) => { // eslint-disable-line no-unused-vars +track(moduleName, 'load_game_bridge', { + sdkVersionTag, +}); + +window.callFunction = async (jsonData: string) => { + // eslint-disable-line no-unused-vars console.log(`Call function ${jsonData}`); let fxName = null; @@ -149,11 +161,12 @@ window.callFunction = async (jsonData: string) => { // eslint-disable-line no-un clientId: request.clientId, audience, scope, - redirectUri: (redirect ?? redirectUri), + redirectUri: redirect ?? redirectUri, logoutRedirectUri: request?.logoutRedirectUri, crossSdkBridgeEnabled: true, }; passportClient = new passport.Passport(passportConfig); + track(moduleName, 'init_inititalise_passport'); } callbackToGame({ responseFor: fxName, @@ -173,6 +186,7 @@ window.callFunction = async (jsonData: string) => { // eslint-disable-line no-un }; console.log(`Version check: ${JSON.stringify(versionCheckParams)}`); + track(moduleName, 'complete_init_game_bridge', versionCheckParams); gameBridgeVersionCheck(versionCheckParams); break; } @@ -190,7 +204,16 @@ window.callFunction = async (jsonData: string) => { // eslint-disable-line no-un break; } case PASSPORT_FUNCTIONS.relogin: { - const userInfo = await passportClient?.login({ useCachedSession: true }); + const userInfo = await passportClient?.login({ + useCachedSession: true, + }); + const succeeded = userInfo !== null; + if (succeeded) { + identify({ passportId: userInfo?.sub }); + } + track(moduleName, 'performed_relogin', { + succeeded, + }); callbackToGame({ responseFor: fxName, requestId, @@ -201,11 +224,17 @@ window.callFunction = async (jsonData: string) => { // eslint-disable-line no-un } case PASSPORT_FUNCTIONS.reconnect: { let providerSet = false; - const userInfo = await passportClient?.login({ useCachedSession: true }); + const userInfo = await passportClient?.login({ + useCachedSession: true, + }); if (userInfo) { const passportProvider = await passportClient?.connectImx(); providerSet = setProvider(passportProvider); + identify({ passportId: userInfo?.sub }); } + track(moduleName, 'performed_reconnect', { + succeeded: userInfo !== null, + }); callbackToGame({ responseFor: fxName, requestId, @@ -226,7 +255,12 @@ window.callFunction = async (jsonData: string) => { // eslint-disable-line no-un } case PASSPORT_FUNCTIONS.loginPKCE: { const request = JSON.parse(data); - await passportClient?.loginWithPKCEFlowCallback(request.authorizationCode, request.state); + const profile = await passportClient?.loginWithPKCEFlowCallback( + request.authorizationCode, + request.state, + ); + identify({ passportId: profile.sub }); + track(moduleName, 'performed_login_pkce'); callbackToGame({ responseFor: fxName, requestId, @@ -236,9 +270,18 @@ window.callFunction = async (jsonData: string) => { // eslint-disable-line no-un } case PASSPORT_FUNCTIONS.connectPKCE: { const request = JSON.parse(data); - await passportClient?.loginWithPKCEFlowCallback(request.authorizationCode, request.state); + const profile = await passportClient?.loginWithPKCEFlowCallback( + request.authorizationCode, + request.state, + ); const passportProvider = await passportClient?.connectImx(); const providerSet = setProvider(passportProvider); + if (providerSet) { + identify({ passportId: profile.sub }); + } + track(moduleName, 'performed_connect_pkce', { + succeeded: providerSet, + }); callbackToGame({ responseFor: fxName, requestId, @@ -249,11 +292,15 @@ window.callFunction = async (jsonData: string) => { // eslint-disable-line no-un } case PASSPORT_FUNCTIONS.loginConfirmCode: { const request = JSON.parse(data); - await passportClient?.loginWithDeviceFlowCallback( + const profile = await passportClient?.loginWithDeviceFlowCallback( request.deviceCode, request.interval, request.timeoutMs ?? null, ); + + identify({ passportId: profile.sub }); + track(moduleName, 'performed_login_confirm_code'); + callbackToGame({ responseFor: fxName, requestId, @@ -263,13 +310,22 @@ window.callFunction = async (jsonData: string) => { // eslint-disable-line no-un } case PASSPORT_FUNCTIONS.connectConfirmCode: { const request = JSON.parse(data); - await passportClient?.loginWithDeviceFlowCallback( + const profile = await passportClient?.loginWithDeviceFlowCallback( request.deviceCode, request.interval, request.timeoutMs ?? null, ); + const passportProvider = await passportClient?.connectImx(); const providerSet = setProvider(passportProvider); + + if (providerSet) { + identify({ passportId: profile.sub }); + } + track(moduleName, 'performed_connect_confirm_code', { + succeeded: providerSet, + }); + callbackToGame({ responseFor: fxName, requestId, @@ -378,7 +434,9 @@ window.callFunction = async (jsonData: string) => { // eslint-disable-line no-un } case PASSPORT_FUNCTIONS.imx.batchNftTransfer: { const nftTransferDetails = JSON.parse(data); - const response = await getProvider().batchNftTransfer(nftTransferDetails); + const response = await getProvider().batchNftTransfer( + nftTransferDetails, + ); callbackToGame({ ...{ responseFor: fxName, diff --git a/packages/game-bridge/tsconfig.json b/packages/game-bridge/tsconfig.json new file mode 100644 index 0000000000..db5668e055 --- /dev/null +++ b/packages/game-bridge/tsconfig.json @@ -0,0 +1,111 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "esnext", /* Specify what module code is generated. */ + // "rootDir": "./src", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + "rootDirs": ["src"], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + "removeComments": false, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + + /* Advanced */ + // "noEmit": true, + // "allowJs": false, + // "allowSyntheticDefaultImports": true, + // "resolveJsonModule": true, + }, + "include": ["src"], + "exclude": ["dist", "jest.config.js", "node_modules"] +} diff --git a/packages/internal/metrics/src/identify.ts b/packages/internal/metrics/src/identify.ts index f24df7092f..7dcbd109ba 100644 --- a/packages/internal/metrics/src/identify.ts +++ b/packages/internal/metrics/src/identify.ts @@ -11,12 +11,12 @@ type Identity = { const parseIdentity = (params: Identity) => { if (params.passportId) { - const key = `passport:${params.passportId}`; + const key = `passport:${params.passportId.toLowerCase()}`; return key; } if (params.ethAddress) { - const key = `ethAddress:${params.ethAddress}`; + const key = `ethAddress:${params.ethAddress.toLowerCase()}`; return key; } diff --git a/packages/internal/metrics/src/index.ts b/packages/internal/metrics/src/index.ts index 76a74778cd..a601fba1dd 100644 --- a/packages/internal/metrics/src/index.ts +++ b/packages/internal/metrics/src/index.ts @@ -1,7 +1,14 @@ -export { track } from './track'; +import { track } from './track'; + export { identify } from './identify'; export { setEnvironment, setPassportClientId, setPublishableApiKey, } from './details'; + +track('metrics', 'sdk_version', { + version: '__SDK_VERSION__', +}); + +export { track }; diff --git a/packages/internal/metrics/src/track.ts b/packages/internal/metrics/src/track.ts index 0d6e09bc36..aee734b9a9 100644 --- a/packages/internal/metrics/src/track.ts +++ b/packages/internal/metrics/src/track.ts @@ -15,7 +15,7 @@ import { POLLING_FREQUENCY } from './utils/constants'; const trackFn = ( moduleName: string, eventName: string, - properties?: Record, + properties?: Record, ) => { const event = { event: `${moduleName}.${eventName}`, diff --git a/packages/internal/metrics/src/utils/state.ts b/packages/internal/metrics/src/utils/state.ts index 640595dcb7..8597b7d027 100644 --- a/packages/internal/metrics/src/utils/state.ts +++ b/packages/internal/metrics/src/utils/state.ts @@ -44,11 +44,16 @@ export const removeSentEvents = (numberOfEvents: number) => { }; export const flattenProperties = ( - properties: Record, + properties: Record, ) => { const propertyMap: [string, string][] = []; Object.entries(properties).forEach(([key, value]) => { - if (typeof value === 'string' || typeof value === 'number') { + if ( + typeof key === 'string' + || typeof value === 'string' + || typeof value === 'number' + || typeof value === 'boolean' + ) { propertyMap.push([key, value.toString()]); } }); diff --git a/packages/passport/sdk/package.json b/packages/passport/sdk/package.json index 6c1f8e7bd3..ef6f91e20f 100644 --- a/packages/passport/sdk/package.json +++ b/packages/passport/sdk/package.json @@ -12,6 +12,7 @@ "@imtbl/core-sdk": "^2.4.0", "@imtbl/generated-clients": "0.0.0", "@imtbl/guardian": "0.0.0", + "@imtbl/metrics": "0.0.0", "@imtbl/toolkit": "0.0.0", "@imtbl/x-client": "0.0.0", "@imtbl/x-provider": "0.0.0", diff --git a/packages/passport/sdk/src/Passport.ts b/packages/passport/sdk/src/Passport.ts index 36920d87c4..1f3c2ceb33 100644 --- a/packages/passport/sdk/src/Passport.ts +++ b/packages/passport/sdk/src/Passport.ts @@ -1,8 +1,15 @@ import { IMXProvider } from '@imtbl/x-provider'; -import { ImxApiClients, imxApiConfig, MultiRollupApiClients } from '@imtbl/generated-clients'; +import { + ImxApiClients, + imxApiConfig, + MultiRollupApiClients, +} from '@imtbl/generated-clients'; import { IMXClient } from '@imtbl/x-client'; import { ChainName } from 'network/chains'; import { Environment } from '@imtbl/config'; + +import { setPassportClientId, identify, track } from '@imtbl/metrics'; + import AuthManager from './authManager'; import MagicAdapter from './magicAdapter'; import { PassportImxProviderFactory } from './starkEx'; @@ -45,12 +52,15 @@ export class Passport { || new IMXClient({ baseConfig: passportModuleConfiguration.baseConfig, }); - this.multiRollupApiClients = new MultiRollupApiClients(this.config.multiRollupConfig); + this.multiRollupApiClients = new MultiRollupApiClients( + this.config.multiRollupConfig, + ); this.passportEventEmitter = new TypedEventEmitter(); const imxClientConfig = this.config.baseConfig.environment === Environment.PRODUCTION - ? imxApiConfig.getProduction() : imxApiConfig.getSandbox(); + ? imxApiConfig.getProduction() + : imxApiConfig.getSandbox(); const imxApiClients = passportModuleConfiguration.overrides?.imxApiClients - || new ImxApiClients(imxClientConfig); + || new ImxApiClients(imxClientConfig); this.passportImxProviderFactory = new PassportImxProviderFactory({ authManager: this.authManager, @@ -61,6 +71,9 @@ export class Passport { passportEventEmitter: this.passportEventEmitter, imxApiClients, }); + + setPassportClientId(passportModuleConfiguration.clientId); + track('passport', 'initialised'); } /** @@ -95,7 +108,7 @@ export class Passport { * @returns {Promise} the user profile if the user is logged in, otherwise null */ public async login(options?: { - useCachedSession: boolean + useCachedSession: boolean; }): Promise { const { useCachedSession = false } = options || {}; let user = null; @@ -112,6 +125,12 @@ export class Passport { user = await this.authManager.login(); } + if (user) { + identify({ + passportId: user.profile.sub, + }); + } + return user ? user.profile : null; } @@ -128,7 +147,11 @@ export class Passport { interval: number, timeoutMs?: number, ): Promise { - const user = await this.authManager.loginWithDeviceFlowCallback(deviceCode, interval, timeoutMs); + const user = await this.authManager.loginWithDeviceFlowCallback( + deviceCode, + interval, + timeoutMs, + ); return user.profile; } @@ -136,8 +159,14 @@ export class Passport { return this.authManager.getPKCEAuthorizationUrl(); } - public async loginWithPKCEFlowCallback(authorizationCode: string, state: string): Promise { - const user = await this.authManager.loginWithPKCEFlowCallback(authorizationCode, state); + public async loginWithPKCEFlowCallback( + authorizationCode: string, + state: string, + ): Promise { + const user = await this.authManager.loginWithPKCEFlowCallback( + authorizationCode, + state, + ); return user.profile; } @@ -198,10 +227,13 @@ export class Passport { return []; } const headers = { Authorization: `Bearer ${user.accessToken}` }; - const linkedAddressesResult = await this.multiRollupApiClients.passportApi.getLinkedAddresses({ - chainName: ChainName.ETHEREUM, - userId: user?.profile.sub, - }, { headers }); + const linkedAddressesResult = await this.multiRollupApiClients.passportApi.getLinkedAddresses( + { + chainName: ChainName.ETHEREUM, + userId: user?.profile.sub, + }, + { headers }, + ); return linkedAddressesResult.data.linked_addresses; } } diff --git a/yarn.lock b/yarn.lock index bd64b26417..bc0e67d56e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3589,10 +3589,12 @@ __metadata: resolution: "@imtbl/game-bridge@workspace:packages/game-bridge" dependencies: "@imtbl/config": 0.0.0 + "@imtbl/metrics": 0.0.0 "@imtbl/passport": 0.0.0 "@imtbl/version-check": 0.0.0 "@imtbl/x-client": 0.0.0 "@imtbl/x-provider": 0.0.0 + eslint: ^8.40.0 parcel: ^2.8.3 languageName: unknown linkType: soft @@ -3710,6 +3712,7 @@ __metadata: "@imtbl/core-sdk": ^2.4.0 "@imtbl/generated-clients": 0.0.0 "@imtbl/guardian": 0.0.0 + "@imtbl/metrics": 0.0.0 "@imtbl/toolkit": 0.0.0 "@imtbl/x-client": 0.0.0 "@imtbl/x-provider": 0.0.0