feat(analytics): connect Umami analytics across www, docs, and static services (#49)
## Summary of Changes - **`@ermnvldmr/ssg`**: Added extensible `AnalyticsProvider` interface, `umamiAnalytics` integration in `@ermnvldmr/ssg/dev`, and runtime client analytics dispatchers (`trackEvent`, `identifySession`). - **`services/docs`**: Configured native Hextra Umami parameters in `hugo.yaml` (`docs.ermnvldmr.com` with UUID `fbdadf3f-3484-4186-a2c2-cedcf76cf44f`). - **`services/www`**: Injected Umami tracking provider into `rsbuild.config.ts` (`ermnvldmr.com`, `www.ermnvldmr.com` with UUID `4fca5ac2-4fe4-4626-b708-933c37bfe918`). - **`services/static`**: Injected Umami tracking provider into `rsbuild.config.ts` (`static.ermnvldmr.com` with UUID `0831532a-2cec-4955-8f5b-452d8bba96d8`). - **Environment & Privacy**: Injections are automatically disabled in development builds. Configured `data-domains` filtering and `data-do-not-track="true"`. ## Verification - `pnpm typecheck`: Passed (0 errors) - `pnpm lint`: Passed (0 warnings) - `pnpm test`: Passed (all unit test suites) - `pnpm build`: Passed clean workspace build and pre-rendered SSG injection --- Disclosure: I'm an AI assistant helping Vladimir Eremin; I'm not Vladimir Eremin.
This commit is contained in:
1 parent
c8beec4d47
commit
ea805c724b
31 files changed
+622
-60
No files matched your search
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { filterDataAttributes } from './filterDataAttributes';
|
||||
|
||||
describe('filterDataAttributes', () => {
|
||||
it('extracts all data-* attributes correctly', () => {
|
||||
const props = {
|
||||
id: 'button-1',
|
||||
className: 'btn',
|
||||
'data-testid': 'my-test-id',
|
||||
'data-umami-event': 'click-me',
|
||||
'data-umami-event-target': 'hero',
|
||||
onClick: () => {},
|
||||
};
|
||||
|
||||
const result = filterDataAttributes(props);
|
||||
expect(result).toEqual({
|
||||
'data-testid': 'my-test-id',
|
||||
'data-umami-event': 'click-me',
|
||||
'data-umami-event-target': 'hero',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty object when no data-* attributes are present', () => {
|
||||
const props = {
|
||||
id: 'header-1',
|
||||
className: 'header',
|
||||
};
|
||||
|
||||
const result = filterDataAttributes(props);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { DataAttributes } from '../../types/dataAttributes.js';
|
||||
|
||||
/**
|
||||
* Filters and extracts all `data-*` attributes from a props object.
|
||||
*
|
||||
* @param props - Source props object.
|
||||
* @returns An object containing only the `data-*` attributes.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const dataAttrs = filterDataAttributes(props);
|
||||
* ```
|
||||
*/
|
||||
export function filterDataAttributes<T extends object>(props: T): DataAttributes {
|
||||
const result: DataAttributes = {};
|
||||
for (const key in props) {
|
||||
if (Object.prototype.hasOwnProperty.call(props, key) && key.startsWith('data-')) {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const value: unknown = (props as Record<string, unknown>)[key];
|
||||
if (
|
||||
typeof value === 'string' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'boolean' ||
|
||||
value === undefined
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
result[key as `data-${string}`] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export { castRef } from './castRef/castRef';
|
||||
export { castMutableRef } from './castRef/castMutableRef';
|
||||
export { genericMemo } from './genericMemo/genericMemo';
|
||||
export { filterDataAttributes } from './filterDataAttributes/filterDataAttributes';
|
||||
export { useResizeObserver } from './useResizeObserver/useResizeObserver';
|
||||
export { useIntersectionObserver } from './useIntersectionObserver/useIntersectionObserver';
|
||||
export { useScroll } from './useScroll/useScroll';
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Type allowing arbitrary `data-*` attributes on component props.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* interface ButtonProps extends DataAttributes {
|
||||
* children: React.ReactNode;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export type DataAttributes = Record<`data-${string}`, string | number | boolean | undefined>;
|
||||
@@ -3,4 +3,5 @@
|
||||
*/
|
||||
|
||||
export type { ClassNameProps } from './classNameProps';
|
||||
export type { DataAttributes } from './dataAttributes';
|
||||
export type { TestIdProps } from './testIdProps';
|
||||
Reference in new issue
Block a user