From fe6d9e13af85c8dc1fbdd5e48e5c870be769ecf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eray=20Ayd=C4=B1n?= Date: Thu, 10 Sep 2026 13:32:11 +0300 Subject: [PATCH 1/4] fix: remove postinstall script and detect framework at runtime pnpm 10+ blocks install scripts by default, so pnpm users hit `ERR_PNPM_IGNORED_BUILDS` and got no framework info in `integrationInfo` at all. The script only stamped the framework name and version into the published bundle, and runtime detection already existed as the fallback. React now reports an exact version it previously lacked, so `integrationInfo` goes from `react-sdk//react` to `react-sdk//react/`. Next.js is unchanged. Preact reports no version, since preact exposes none at runtime. Related-Task: INTER-2492 --- .changeset/lucky-donuts-repeat.md | 9 +++ __tests__/detect-env.test.ts | 64 ++-------------------- __tests__/fpjs-provider.test.tsx | 4 +- __tests__/with-environment.preact.test.tsx | 13 +++++ package.json | 4 +- scripts/detect-env.js | 58 -------------------- src/components/with-environment.tsx | 5 +- src/detect-env.ts | 3 + src/env.ts | 1 - src/env.types.ts | 4 -- src/get-env.ts | 16 ------ 11 files changed, 34 insertions(+), 147 deletions(-) create mode 100644 .changeset/lucky-donuts-repeat.md delete mode 100644 scripts/detect-env.js delete mode 100644 src/env.ts delete mode 100644 src/get-env.ts diff --git a/.changeset/lucky-donuts-repeat.md b/.changeset/lucky-donuts-repeat.md new file mode 100644 index 00000000..0dcbb0ce --- /dev/null +++ b/.changeset/lucky-donuts-repeat.md @@ -0,0 +1,9 @@ +--- +'@fingerprint/react': minor +--- + +Remove the `postinstall` script and detect the framework at runtime instead. + +pnpm 10+ blocks install scripts by default, so pnpm users hit `[ERR_PNPM_IGNORED_BUILDS]` and got no framework info in `integrationInfo` at all. + +React apps now report an exact version they previously lacked, so `integrationInfo` goes from `react-sdk//react` to `react-sdk//react/`. Next.js is unchanged. Preact reports no version, since preact exposes none at runtime. diff --git a/__tests__/detect-env.test.ts b/__tests__/detect-env.test.ts index 78d7f5e8..db39012a 100644 --- a/__tests__/detect-env.test.ts +++ b/__tests__/detect-env.test.ts @@ -1,3 +1,5 @@ +import { version as reactVersion } from 'react' + import { detectEnvironment } from '../src/detect-env' import { Env } from '../src/env.types' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -33,6 +35,7 @@ describe('Detect user env', () => { expect(env).toEqual({ name: Env.React, + version: reactVersion, }) }) }) @@ -88,6 +91,7 @@ describe('Detect user env', () => { expect(env).toEqual({ name: Env.React, + version: reactVersion, }) } finally { if (originalWindowDescriptor) { @@ -99,63 +103,3 @@ describe('Detect user env', () => { }) }) }) - -describe('getEnvironment', () => { - afterEach(() => { - vi.resetModules() - vi.doUnmock('../src/env') - }) - - it('returns parsed env details when the build-time env placeholder is valid JSON', async () => { - vi.resetModules() - vi.doMock('../src/env', () => ({ - env: JSON.stringify({ name: 'react', version: '18.0.0' }), - })) - - const { getEnvironment: getEnvironmentFresh } = await import('../src/get-env') - - expect( - getEnvironmentFresh({ - context: { classRenderReceivesAnyArguments: false }, - }) - ).toEqual({ - name: 'react', - version: '18.0.0', - }) - }) - - it('falls back to detection when the build-time env JSON is not env details', async () => { - vi.resetModules() - vi.doMock('../src/env', () => ({ - env: JSON.stringify({ foo: 'bar' }), - })) - - const { getEnvironment: getEnvironmentFresh } = await import('../src/get-env') - - expect( - getEnvironmentFresh({ - // absence of classRenderReceivesAnyArguments is React signal - context: { classRenderReceivesAnyArguments: false }, - }) - ).toEqual({ - name: Env.React, - }) - }) - - it('falls back to detection when the build-time env is invalid', async () => { - vi.resetModules() - vi.doMock('../src/env', () => ({ - env: '%DETECTED_ENV%', - })) - - const { getEnvironment: getEnvironmentFresh } = await import('../src/get-env') - - expect( - getEnvironmentFresh({ - context: { classRenderReceivesAnyArguments: false }, - }) - ).toEqual({ - name: Env.React, - }) - }) -}) diff --git a/__tests__/fpjs-provider.test.tsx b/__tests__/fpjs-provider.test.tsx index a09068db..4065b7c1 100644 --- a/__tests__/fpjs-provider.test.tsx +++ b/__tests__/fpjs-provider.test.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren, useContext } from 'react' +import { PropsWithChildren, useContext, version as reactVersion } from 'react' import { act, render, renderHook } from '@testing-library/react' import { FingerprintContext, FingerprintProvider, FingerprintProviderOptions, useVisitorData } from '../src' import { createWrapper, getDefaultStartOptions } from './helpers' @@ -67,7 +67,7 @@ describe('FingerprintProvider', () => { }) expect(mockStart).toHaveBeenCalledWith({ ...loadOptions, - integrationInfo: [`react-sdk/${version}/react`], + integrationInfo: [`react-sdk/${version}/react/${reactVersion}`], cache: { cachePrefix: 'cache', storage: 'sessionStorage', diff --git a/__tests__/with-environment.preact.test.tsx b/__tests__/with-environment.preact.test.tsx index 690d9f78..94da9150 100644 --- a/__tests__/with-environment.preact.test.tsx +++ b/__tests__/with-environment.preact.test.tsx @@ -22,5 +22,18 @@ describe('WithEnvironment', () => { expect(container.innerHTML).toContain('preact') }) + + it('should not report a version', async () => { + const { WithEnvironment } = await import('../src/components/with-environment') + const PrintEnv = (props: { env: { version?: string } }) => + h('div', null, JSON.stringify(props.env.version ?? null)) + + // @ts-expect-error -- preact's render signature does not match React's @testing-library/react types + const { container } = preactRender(h(WithEnvironment, { children: (env) => h(PrintEnv, { env }) })) + + // `compat.version` is the hardcoded React-compatibility number and must never leak through + expect(compat.version).toBeDefined() + expect(container.innerHTML).toBe('
null
') + }) }) }) diff --git a/package.json b/package.json index f14edf40..005c614d 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "privacy" ], "scripts": { - "postinstall": "node scripts/detect-env.js", "prepare": "husky", "watch": "vite build --watch", "build": "vite build", @@ -45,8 +44,7 @@ "changeset:publish": "HUSKY=0 changeset publish" }, "files": [ - "dist", - "scripts" + "dist" ], "repository": { "type": "git", diff --git a/scripts/detect-env.js b/scripts/detect-env.js deleted file mode 100644 index e1462dbd..00000000 --- a/scripts/detect-env.js +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ -const fs = require('fs') -const pkg = require('../package.json') -const path = require('path') -const cwd = process.env.INIT_CWD || path.resolve('../../', __dirname) - -const token = '%DETECTED_ENV%' - -const Env = { - React: 'react', - Preact: 'preact', - Next: 'next', - Unknown: 'unknown', -} - -const distPaths = [pkg.main, pkg.module] - -const envDetectOrder = [Env.Next, Env.Preact, Env.React] - -function detectEnv(env) { - try { - const pkgPath = path.join(cwd, `node_modules/${env}/package.json`) - const pkg = require(pkgPath) - - return pkg.version - } catch { - return null - } -} - -function writeDetectedEnv(env, version) { - distPaths.forEach((distPath) => { - const fullDistPath = path.resolve(__dirname, '../', distPath) - - const contents = fs.readFileSync(fullDistPath).toString() - const newContents = contents.replace(token, JSON.stringify({ name: env, version }).replace(/"/g, '\\"')) - - fs.writeFileSync(fullDistPath, newContents) - }) -} - -function main() { - for (const env of envDetectOrder) { - const version = detectEnv(env) - - if (version) { - writeDetectedEnv(env, version) - - return - } - } -} - -try { - main() -} catch { - // Nothing here... -} diff --git a/src/components/with-environment.tsx b/src/components/with-environment.tsx index 833fd07c..037001d4 100644 --- a/src/components/with-environment.tsx +++ b/src/components/with-environment.tsx @@ -1,7 +1,6 @@ import { Component, type JSX } from 'react' -import { getEnvironment } from '../get-env' -import { type DetectEnvParams } from '../detect-env' +import { detectEnvironment, type DetectEnvParams } from '../detect-env' import { type EnvDetails } from '../env.types' export interface WithEnvironmentProps { @@ -31,7 +30,7 @@ class WithEnvironment extends Component { context: { classRenderReceivesAnyArguments: hasAnyArguments }, } - this.detectedEnv = getEnvironment(detectParams) + this.detectedEnv = detectEnvironment(detectParams) } return this.props.children(this.detectedEnv) diff --git a/src/detect-env.ts b/src/detect-env.ts index 52ceeed4..e5de5190 100644 --- a/src/detect-env.ts +++ b/src/detect-env.ts @@ -1,3 +1,5 @@ +import { version as reactVersion } from 'react' + import { Env, type EnvDetails } from './env.types' export interface DetectEnvContext { @@ -81,6 +83,7 @@ export function detectEnvironment({ context }: DetectEnvParams): EnvDetails { if (isReact(context)) { return { name: Env.React, + version: reactVersion, } } diff --git a/src/env.ts b/src/env.ts deleted file mode 100644 index 42517067..00000000 --- a/src/env.ts +++ /dev/null @@ -1 +0,0 @@ -export const env = '%DETECTED_ENV%' diff --git a/src/env.types.ts b/src/env.types.ts index 95038743..62194bb1 100644 --- a/src/env.types.ts +++ b/src/env.types.ts @@ -9,7 +9,3 @@ export interface EnvDetails { name: Env version?: string } - -export function isEnvDetails(value: unknown): value is EnvDetails { - return typeof value === 'object' && value !== null && 'name' in value -} diff --git a/src/get-env.ts b/src/get-env.ts deleted file mode 100644 index 65f4f0cf..00000000 --- a/src/get-env.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { env } from './env' -import { EnvDetails, isEnvDetails } from './env.types' -import { detectEnvironment, type DetectEnvParams } from './detect-env' - -export function getEnvironment(params: DetectEnvParams): EnvDetails { - try { - const jsonEnv: unknown = JSON.parse(env) - if (isEnvDetails(jsonEnv)) { - return jsonEnv - } - } catch { - // Nothing here... - } - - return detectEnvironment(params) -} From af0a5aa9b3b7c93f9c6dffb409e7e42b666406f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eray=20Ayd=C4=B1n?= Date: Thu, 10 Sep 2026 15:26:35 +0300 Subject: [PATCH 2/4] test: cover version-less integrationInfo --- __tests__/fpjs-provider.test.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/__tests__/fpjs-provider.test.tsx b/__tests__/fpjs-provider.test.tsx index 4065b7c1..3c96e3cc 100644 --- a/__tests__/fpjs-provider.test.tsx +++ b/__tests__/fpjs-provider.test.tsx @@ -7,6 +7,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import * as agent from '@fingerprint/agent' import type { GetOptions } from '@fingerprint/agent' import * as ssr from '../src/ssr' +import * as detectEnv from '../src/detect-env' +import { Env } from '../src/env.types' vi.mock('@fingerprint/agent', { spy: true }) @@ -88,6 +90,18 @@ describe('FingerprintProvider', () => { ) }) + it('should omit the version in integrationInfo when the framework exposes none', () => { + vi.spyOn(detectEnv, 'detectEnvironment').mockReturnValue({ name: Env.Preact }) + + renderProvider() + + expect(mockStart).toHaveBeenCalledWith( + expect.objectContaining({ + integrationInfo: [`react-sdk/${version}/preact`], + }) + ) + }) + it('should rebuild the agent when forceRebuild is enabled and options change', () => { const { rerender } = renderProvider({ apiKey: 'key-a', forceRebuild: true }) From f9bfbb4f2370987bdb7fde127c2d20690cae77c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eray=20Ayd=C4=B1n?= Date: Thu, 10 Sep 2026 15:30:37 +0300 Subject: [PATCH 3/4] refactor: drop unreachable Env.Unknown branch --- src/detect-env.ts | 17 ++--------------- src/env.types.ts | 1 - 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/detect-env.ts b/src/detect-env.ts index e5de5190..2c2a76ad 100644 --- a/src/detect-env.ts +++ b/src/detect-env.ts @@ -36,13 +36,6 @@ function isPreact(context: DetectEnvContext) { return context.classRenderReceivesAnyArguments } -/** - * Checks if user is using react. - * */ -function isReact(context: DetectEnvContext) { - return !context.classRenderReceivesAnyArguments -} - /** * Runs checks that determine if user is using next. * Those checks should have almost 100% accuracy. @@ -80,14 +73,8 @@ export function detectEnvironment({ context }: DetectEnvParams): EnvDetails { } } - if (isReact(context)) { - return { - name: Env.React, - version: reactVersion, - } - } - return { - name: Env.Unknown, + name: Env.React, + version: reactVersion, } } diff --git a/src/env.types.ts b/src/env.types.ts index 62194bb1..44182133 100644 --- a/src/env.types.ts +++ b/src/env.types.ts @@ -2,7 +2,6 @@ export enum Env { React = 'react', Preact = 'preact', Next = 'next', - Unknown = 'unknown', } export interface EnvDetails { From 35454c72aecbeeadb8609fc832b0861dec8d0c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eray=20Ayd=C4=B1n?= Date: Mon, 14 Sep 2026 13:14:47 +0300 Subject: [PATCH 4/4] chore: simplify changeset release notes --- .changeset/lucky-donuts-repeat.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.changeset/lucky-donuts-repeat.md b/.changeset/lucky-donuts-repeat.md index 0dcbb0ce..84e3c74a 100644 --- a/.changeset/lucky-donuts-repeat.md +++ b/.changeset/lucky-donuts-repeat.md @@ -2,8 +2,4 @@ '@fingerprint/react': minor --- -Remove the `postinstall` script and detect the framework at runtime instead. - -pnpm 10+ blocks install scripts by default, so pnpm users hit `[ERR_PNPM_IGNORED_BUILDS]` and got no framework info in `integrationInfo` at all. - -React apps now report an exact version they previously lacked, so `integrationInfo` goes from `react-sdk//react` to `react-sdk//react/`. Next.js is unchanged. Preact reports no version, since preact exposes none at runtime. +Remove the `postinstall` script for detecting framework version.