Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
00fed2d
feat(premium-analytics): add tsconfig paths and typecheck for interna…
chihsuan May 27, 2026
20e6241
Potential fix for pull request finding
chihsuan May 27, 2026
662c887
Potential fix for pull request finding
chihsuan May 27, 2026
d4da9ca
docs(premium-analytics): clarify internal-package naming and rename init
chihsuan May 28, 2026
b5893ea
test(premium-analytics): add jest infrastructure for internal packages
chihsuan May 29, 2026
7aa601a
feat(premium-analytics): add site-sync package
chihsuan May 29, 2026
8426659
feat(premium-analytics): configure apiFetch auth in init module
chihsuan May 29, 2026
adbed20
changelog: add premium-analytics site-sync entry
chihsuan May 29, 2026
528458b
Merge branch 'trunk' into wooa7s-1320-configure-workspace-and-typescr…
chihsuan Jun 2, 2026
8b78bee
Merge remote-tracking branch 'origin/wooa7s-1320-configure-workspace-…
chihsuan Jun 2, 2026
feeb212
docs(premium-analytics): revert internal-packages README section
chihsuan Jun 3, 2026
2beeaa9
fix(premium-analytics): align route name with internal-package conven…
chihsuan Jun 3, 2026
5359e15
Merge remote-tracking branch 'origin/wooa7s-1320-configure-workspace-…
chihsuan Jun 3, 2026
952a10c
Merge remote-tracking branch 'origin/trunk' into wooa7s-1320-configur…
chihsuan Jun 5, 2026
ff7077f
Merge remote-tracking branch 'origin/wooa7s-1320-configure-workspace-…
chihsuan Jun 5, 2026
2de7cca
Merge remote-tracking branch 'origin/trunk' into wooa7s-1321-integrat…
chihsuan Jun 5, 2026
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
33 changes: 33 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions projects/packages/premium-analytics/changelog/add-site-sync
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add site-sync package (useSyncStatus hook) and configure apiFetch auth in init.
21 changes: 21 additions & 0 deletions projects/packages/premium-analytics/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const baseConfig = require( 'jetpack-js-tools/jest/config.base.js' );

module.exports = {
...baseConfig,
rootDir: '.',
collectCoverageFrom: [
'<rootDir>/packages/**/src/**/*.{js,jsx,ts,tsx}',
...baseConfig.collectCoverageFrom,
],
// Compile TS/JSX to CommonJS (rather than leaving native ESM). `jest.mock`
// hoisting relies on `require`, which is undefined when `.ts` runs as ESM
// under `--experimental-vm-modules`; emitting CJS keeps mocks working without
// that flag. Mirrors the shared transform used by js-packages/charts.
transform: {
...baseConfig.transform,
'\\.[jt]sx?$': require( 'jetpack-js-tools/jest/babel-jest-config-factory.js' )(
require.resolve
),
},
extensionsToTreatAsEsm: [],
};
14 changes: 13 additions & 1 deletion projects/packages/premium-analytics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"build": "wp-build && mkdir -p build/modules/boot && cp shims/boot-asset.php build/modules/boot/index.min.asset.php",
"build-production": "NODE_ENV=production wp-build && mkdir -p build/modules/boot && cp shims/boot-asset.php build/modules/boot/index.min.asset.php",
"test": "jest",
"typecheck": "tsgo --noEmit",
"watch": "wp-build --watch"
},
Expand All @@ -29,6 +30,8 @@
}
},
"dependencies": {
"@automattic/jetpack-script-data": "workspace:*",
"@wordpress/api-fetch": "^7.22.0",
"@wordpress/boot": "0.13.0",
"@wordpress/data": "10.46.0",
"@wordpress/i18n": "^6.9.0",
Expand All @@ -38,9 +41,18 @@
"react-dom": "18.3.1"
},
"devDependencies": {
"@automattic/jetpack-webpack-config": "workspace:*",
"@babel/core": "7.29.0",
"@babel/preset-react": "7.28.5",
"@babel/preset-typescript": "7.28.5",
"@babel/runtime": "7.29.2",
"@testing-library/dom": "10.4.1",
"@testing-library/react": "16.3.2",
"@types/jest": "30.0.0",
"@typescript/native-preview": "7.0.0-dev.20260225.1",
"@wordpress/build": "0.14.0",
"browserslist": "4.28.2"
"babel-jest": "30.4.1",
"browserslist": "4.28.2",
"jest": "30.4.2"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"module": "build-module/index.mjs",
"wpScriptModuleExports": "./build-module/index.mjs",
"dependencies": {
"@automattic/jetpack-script-data": "workspace:*",
"@wordpress/api-fetch": "^7.22.0",
"@wordpress/boot": "0.13.0",
"@wordpress/data": "10.46.0",
"@wordpress/icons": "^13.0.0"
Expand Down
12 changes: 12 additions & 0 deletions projects/packages/premium-analytics/packages/init/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* External dependencies
*/
import { getScriptData } from '@automattic/jetpack-script-data';
import apiFetch from '@wordpress/api-fetch';
import { store as bootStore } from '@wordpress/boot';
import { dispatch } from '@wordpress/data';
import { chartBar } from '@wordpress/icons';
Expand All @@ -10,6 +12,16 @@ import { chartBar } from '@wordpress/icons';
* Runs before routes render.
*/
export async function init(): Promise< void > {
// Point apiFetch at this site's REST API and authenticate requests. Required
// before any package (e.g. site-sync) calls apiFetch against /jetpack/v4/*.
const site = getScriptData()?.site;
if ( site?.rest_root ) {
apiFetch.use( apiFetch.createRootURLMiddleware( site.rest_root ) );
}
if ( site?.rest_nonce ) {
apiFetch.use( apiFetch.createNonceMiddleware( site.rest_nonce ) );
}

dispatch( bootStore ).updateMenuItem( 'dashboard', {
icon: chartBar,
} );
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"private": true,
"name": "@automattic/jetpack-premium-analytics-site-sync",
"version": "0.1.0",
"type": "module",
"wpScript": true,
"module": "build-module/index.mjs",
"wpScriptModuleExports": "./build-module/index.mjs",
"dependencies": {
"@automattic/jetpack-script-data": "workspace:*",
"@wordpress/api-fetch": "^7.22.0",
"@wordpress/i18n": "^6.9.0",
"react": "18.3.1"
},
"devDependencies": {
"@testing-library/react": "16.3.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { SYNC_STATUS_PATH } from '../constants';
import type { SyncStatusApiResponse } from '../types';

/**
* Fetch the current sync status from Jetpack core.
*
* @return The current sync status.
*/
export function fetchSyncStatus(): Promise< SyncStatusApiResponse > {
return apiFetch( { path: SYNC_STATUS_PATH } );
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { FULL_SYNC_PATH } from '../constants';

/**
* Trigger a Jetpack full sync.
*
* @return The full-sync trigger response.
*/
export function triggerFullSync(): Promise< unknown > {
return apiFetch( { path: FULL_SYNC_PATH, method: 'POST' } );
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Polling interval in milliseconds.
*/
export const POLL_INTERVAL = 3_000;

/**
* Jetpack core sync status endpoint (queue + full-sync state).
*/
export const SYNC_STATUS_PATH = '/jetpack/v4/sync/status';

/**
* Jetpack core full-sync trigger endpoint.
*/
export const FULL_SYNC_PATH = '/jetpack/v4/sync/full-sync';

/**
* Sync-module key whose progress gates the analytics dashboard. Mirrors the
* backend default (`Sync_Status_Tracker::ANALYTICS_SYNC_MODULE`).
*/
export const ANALYTICS_SYNC_MODULE = 'woocommerce_analytics';
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* External dependencies
*/
import { getScriptData } from '@automattic/jetpack-script-data';
import { renderHook, act, waitFor } from '@testing-library/react';
/**
* Internal dependencies
*/
import { fetchSyncStatus } from '../../api/fetch-sync-status';
import { triggerFullSync } from '../../api/trigger-full-sync';
import { useSyncStatus } from '../use-sync-status';
import type { SyncStatusApiResponse } from '../../types';

jest.mock( '../../api/fetch-sync-status' );
jest.mock( '../../api/trigger-full-sync' );
jest.mock( '@automattic/jetpack-script-data' );

const mockFetch = fetchSyncStatus as jest.MockedFunction< typeof fetchSyncStatus >;
const mockTrigger = triggerFullSync as jest.MockedFunction< typeof triggerFullSync >;
const mockScriptData = getScriptData as jest.MockedFunction< typeof getScriptData >;

/**
* Build a raw sync-status API response for tests.
*
* @param overrides - Fields to override on the default running-analytics response.
* @return A raw sync-status API response.
*/
function rawStatus( overrides: Partial< SyncStatusApiResponse > = {} ): SyncStatusApiResponse {
return {
started: true,
finished: false,
progress: { woocommerce_analytics: { sent: 1, total: 2 } },
...overrides,
};
}

beforeEach( () => {
jest.useFakeTimers();
// Default: milestone not set.
mockScriptData.mockReturnValue( {
premium_analytics: { initial_full_sync_finished: 0 },
} as ReturnType< typeof getScriptData > );
mockFetch.mockResolvedValue( rawStatus() );
mockTrigger.mockResolvedValue( undefined );
} );

afterEach( () => {
jest.clearAllTimers();
jest.useRealTimers();
jest.clearAllMocks();
} );

describe( 'useSyncStatus', () => {
it( 'exposes normalized progress after the first poll', async () => {
const { result } = renderHook( () => useSyncStatus() );

await waitFor( () => expect( result.current.isLoading ).toBe( false ) );
expect( result.current.data?.percentage ).toBe( 50 );
expect( result.current.data?.isRunning ).toBe( true );
expect( result.current.error ).toBeNull();
} );

it( 'reports complete and stops polling when analytics reaches 100', async () => {
mockFetch.mockResolvedValue(
rawStatus( {
finished: true,
progress: { woocommerce_analytics: { sent: 2, total: 2 } },
} )
);
const { result } = renderHook( () => useSyncStatus() );

await waitFor( () => expect( result.current.isComplete ).toBe( true ) );
const callsAfterComplete = mockFetch.mock.calls.length;

await act( async () => {
jest.advanceTimersByTime( 10_000 );
} );
expect( mockFetch.mock.calls ).toHaveLength( callsAfterComplete );
} );

it( 'flags a stalled sync with an error', async () => {
mockFetch.mockResolvedValue(
rawStatus( {
started: true,
finished: true,
progress: { woocommerce_analytics: { sent: 1, total: 2 } },
} )
);
const { result } = renderHook( () => useSyncStatus() );

await waitFor( () => expect( result.current.isStalled ).toBe( true ) );
expect( result.current.error ).toBeInstanceOf( Error );
} );

it( 'surfaces fetch errors and never rejects triggerSync', async () => {
mockFetch.mockRejectedValueOnce( new Error( 'boom' ) );
const { result } = renderHook( () => useSyncStatus() );

await waitFor( () => expect( result.current.error ).toBeInstanceOf( Error ) );
expect( result.current.error?.message ).toBe( 'boom' );

// triggerSync resolves even if the trigger call fails.
mockTrigger.mockRejectedValueOnce( new Error( 'nope' ) );
await act( async () => {
await result.current.triggerSync();
} );
expect( result.current.error?.message ).toBe( 'nope' );
} );

it( 'starts complete and skips polling when the milestone is set', async () => {
mockScriptData.mockReturnValue( {
premium_analytics: { initial_full_sync_finished: 1_700_000_000 },
} as ReturnType< typeof getScriptData > );

const { result } = renderHook( () => useSyncStatus() );

await waitFor( () => expect( result.current.isComplete ).toBe( true ) );
expect( mockFetch ).not.toHaveBeenCalled();
} );
} );
Loading
Loading