npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@doghouse/matomo-form-analytics-custom-field-tracker

v1.0.4

Published

A modular, object-oriented system for creating custom field trackers that integrate with Matomo FormAnalytics

Readme

Matomo Form Analytics Custom Field Tracker

npm version License: MIT GitHub stars GitHub issues

A modular, object-oriented npm package for creating custom field trackers that integrate with Matomo FormAnalytics. Built with inheritance patterns and centralized factory design.

🎯 What This Package Does

This package extends Matomo FormAnalytics to track custom form fields that aren't natively supported, such as:

  • WYSIWYG editors (ProseMirror, TinyMCE, etc.)
  • Star rating systems
  • Image selection interfaces
  • Custom buttons and interactive elements
  • Any custom interactive form elements

✨ Key Features

  • 🔧 Modular Architecture - Easy to extend with new field types
  • 🎯 Object-Oriented Design - Clean inheritance patterns
  • 📊 Full Matomo Integration - Compatible with FormAnalytics API
  • 🚀 Multiple Build Formats - ESM, CommonJS, and UMD support
  • 📝 TypeScript Support - Complete type definitions included
  • 🧪 Comprehensive Testing - Jest test suite with coverage
  • 📚 Well Documented - Extensive examples and API reference
  • 🐛 Debug Support - Built-in debug logging for development
  • 📦 Sample Implementations - Reference examples for common field types
  • 🧹 Automatic Cleanup - Memory leak prevention with tracked event listeners and timers
  • 🔄 Dynamic Field Detection - Automatic support for conditional fields and paginated forms

🚀 Installation

npm install @doghouse/matomo-form-analytics-custom-field-tracker

📦 Usage

Creating Custom Fields

To track custom form fields, you need to create a field class that extends BaseField. Here's how:

1. Create Your Custom Field Class

Create a new file for your custom field (e.g., MyCustomField.js):

import { BaseField } from '@doghouse/matomo-form-analytics-custom-field-tracker';

export class H2ClickField extends BaseField {
  static fieldType = 'h2Click';
  static category = BaseField.BaseField.FieldCategories.SELECTABLE;
  static selector = '.survey-full__intro[data-name]';
  
  constructor(tracker, element, fieldName) {
    super(tracker, element, fieldName);
    this.h2Element = this.getInteractiveElement();
    this.clickCount = 0;
  }
  
  getInteractiveElement() {
    return this.element.querySelector('h2');
  }
  
  isBlank() {
    return this.clickCount === 0;
  }
  
  getFieldSize() {
    return this.clickCount;
  }
  
  setupEventListeners() {
    if (!this.h2Element) return;
    
    this._addTrackedEventListener(this.h2Element, 'click', () => {
      this.onFocus();
      this.clickCount++;
      this.onChange();
      this._trackTimer(setTimeout(() => this.onBlur(), 100));
    });
  }
}

Required Static Properties

Every custom field must define these three static properties:

export class H2ClickField extends BaseField {
  static fieldType = 'h2Click';                    // Unique identifier
  static category = BaseField.FieldCategories.SELECTABLE;  // Field category
  static selector = '.survey-full__intro[data-name]';       // CSS selector
  // ... rest of implementation
}

Property explanations:

  • static fieldType - Unique identifier for your field type (e.g., 'h2Click', 'rating', 'wysiwyg'). Used internally to identify and register your field type. Must be unique across all your custom fields.
  • static category - Field category from BaseField.FieldCategories enum (TEXT, SELECTABLE, or CHECKABLE). Tells Matomo how to categorize this field for analytics. Choose from TEXT (text input), SELECTABLE (dropdowns, ratings), or CHECKABLE (checkboxes, image selectors).
  • static selector - CSS selector to find elements on the page (e.g., '.survey-full__intro[data-name]'). CSS selector that finds the DOM elements this field should track. Should target elements with data-name attributes for proper field identification.

Required Overrides

Every custom field must implement these three abstract methods:

  • getInteractiveElement() - Returns the DOM element that users interact with. This is the actual clickable/typeable element inside your field container. For example, in a WYSIWYG editor, this would be the contenteditable div, not the outer wrapper.
  • isBlank() - Determines if the field is empty/unused. Returns true if the field has no content or user input. Used by Matomo to track completion rates and identify abandoned fields.
  • getFieldSize() - Returns the field's content size/value. This could be character count for text fields, number of selected items for multi-selects, or rating value for star ratings. Used by Matomo to analyze field complexity and user engagement.

Optional Overrides

  • setupEventListeners() - Override this when you need custom event handling beyond the default focus/blur/change events

Memory Management & Cleanup

The BaseField class includes automatic memory leak prevention through tracked event listeners, timers, and MutationObservers:

  • _addTrackedEventListener(element, event, handler, options) - Use this instead of addEventListener() to automatically track listeners for cleanup
  • _trackTimer(timerId) - Use this to wrap setTimeout() or setInterval() calls for automatic cleanup
  • _trackMutationObserver(observer) - Use this to track MutationObserver instances for automatic cleanup
  • _setupTrackedMutationObserver(callback, observeOptions, targetElement) - Convenient helper to create, track, and observe with a MutationObserver (handles disconnecting existing observer if called multiple times)
  • destroy() - Automatically removes all tracked event listeners, clears all timers, and disconnects all MutationObservers

Example with proper cleanup:

setupEventListeners() {
  if (!this.h2Element) return;
  
  // Use tracked event listener (automatically cleaned up)
  this._addTrackedEventListener(this.h2Element, 'click', () => {
    this.onFocus();
    this.clickCount++;
    this.onChange();
    // Use tracked timer (automatically cleaned up)
    this._trackTimer(setTimeout(() => this.onBlur(), 100));
  });
}

setupMutationObserver() {
  const container = this.element.querySelector('.container');
  if (!container) return;
  
  // Option 1: Use _setupTrackedMutationObserver helper (recommended for single observer)
  this._setupTrackedMutationObserver(
    () => this.checkStateChanges(),
    {
      attributes: true,
      attributeFilter: ['class'],
      childList: true,
      subtree: true,
    },
    container
  );
  
  // Option 2: Manual setup with _trackMutationObserver (for multiple observers)
  // const observer = this._trackMutationObserver(new MutationObserver(() => {
  //   this.checkStateChanges();
  // }));
  // observer.observe(container, { attributes: true, childList: true, subtree: true });
}

Benefits:

  • ✅ Automatic cleanup prevents memory leaks
  • ✅ Idempotent (safe to call destroy() multiple times)
  • ✅ No orphaned event listeners, timers, or MutationObservers
  • ✅ Consistent cleanup pattern across all field types

Debug Logging

When implementing custom logic, use conditional debug logging:

setupEventListeners() {
  if (!this.h2Element) {
    this.debug && console.log('H2 element not found');
    return;
  }
  
  this._addTrackedEventListener(this.h2Element, 'click', () => {
    this.debug && console.log(`H2 clicked: ${this.fieldName}`);
    this.onFocus();
    this.clickCount++;
    this.onChange();
    this._trackTimer(setTimeout(() => this.onBlur(), 100));
  });
}

Important: Always use this.debug && console.log(...) for debug output to respect the global debug setting.

2. Initialize the Tracker

After creating your custom field classes, initialize the tracker:

import FormAnalyticsCustomFieldTracker from '@doghouse/matomo-form-analytics-custom-field-tracker';
import { H2ClickField } from './SampleCustom/H2ClickField.js';
import { RatingField } from './SampleCustom/RatingField.js';
import { WysiwygField } from './SampleCustom/WysiwygField.js';
import { ImageSelectorField } from './SampleCustom/ImageSelectorField.js';

// Initialize custom field tracking for unsupported field types
FormAnalyticsCustomFieldTracker.init([
    { fieldType: 'h2Click', FieldClass: H2ClickField },
    { fieldType: 'rating', FieldClass: RatingField },
    { fieldType: 'wysiwyg', FieldClass: WysiwygField },
    { fieldType: 'imageSelector', FieldClass: ImageSelectorField },
], true); // Enable debug logging

Debug Mode

Enable debug logging to see detailed information about field tracking. Debug mode is controlled globally and affects all field instances:

// Enable debug logging
FormAnalyticsCustomFieldTracker.init(customFields, true);

// Disable debug logging (default)
FormAnalyticsCustomFieldTracker.init(customFields, false);

When debug mode is enabled, you'll see console messages like:

  • ✅ Integrated custom rating field: cmF0aW5nOjIwMjUtMTAtMjBUMDE6MzA6MzUuOTUzWg==
  • ⚡️ WYSIWYG focus (wysiwyg-field)
  • ⚡️ RATING changed from 3 to 4 (rating-field)
  • ⚡️ BUTTON click (button-field)

Debug output includes:

  • Form detection and processing
  • Field type matching and integration
  • User interactions (focus, blur, change)
  • Error messages and warnings

Conditional Fields & Paginated Forms

The tracker automatically supports conditional fields and paginated forms through dynamic field detection.

Conditional Fields

Conditional fields are hidden fields that appear dynamically based on another field's value. For example, if a user selects "Yes" to a question, additional fields may appear that weren't visible initially.

The tracker uses a MutationObserver to automatically detect when new fields are added to the form and integrates them seamlessly:

// Example: A conditional field appears when user selects an option
// The tracker automatically detects and starts tracking it
FormAnalyticsCustomFieldTracker.init([
    { fieldType: 'rating', FieldClass: RatingField },
    { fieldType: 'wysiwyg', FieldClass: WysiwygField },
], true);

// When a conditional field appears (e.g., after selecting "Yes"),
// it's automatically detected and tracked without any additional code

How it works:

  • The tracker monitors the form for DOM changes
  • When new fields matching your custom field selectors are added, they're automatically detected
  • Both native Matomo fields and custom fields are re-scanned
  • Fields are only tracked once (duplicate detection prevents double-tracking)

Paginated Forms

Paginated forms are multi-step forms where fields are added to the DOM as users navigate through pages. The tracker handles this automatically:

// Works seamlessly with multi-step/paginated forms
FormAnalyticsCustomFieldTracker.init([
    { fieldType: 'rating', FieldClass: RatingField },
    { fieldType: 'wysiwyg', FieldClass: WysiwygField },
], true);

// As users navigate to page 2, 3, etc., new fields are automatically detected

Features:

  • ✅ Automatic detection of fields added on new pages
  • ✅ Debounced re-scanning (300ms) to handle rapid changes efficiently
  • ✅ Works with both native Matomo fields and custom fields
  • ✅ No additional configuration needed

Debug output for dynamic fields: When debug mode is enabled, you'll see messages like:

  • 📄 New fields detected (pagination/conditional), re-scanning...
  • 🔄 Re-scanned native tracker for new fields
  • 👀 Set up dynamic field observer for pagination/conditional fields
  • ✅ Integrated custom rating field: field-name (for newly detected fields)

Technical details:

  • Uses MutationObserver API to watch for DOM changes
  • Observes the entire form subtree for added nodes
  • Detects standard form fields (input, select, textarea) and custom field containers
  • Debounces re-scanning to optimize performance
  • Prevents duplicate tracking by checking if fields are already tracked

📁 Project Structure

src/
├── BaseField.js                    # Base class for custom fields
├── FormAnalyticsCustomFieldTracker.js  # Main tracker with field management
├── Enums/
│   └── FieldCategories.js         # Field category definitions
├── examples/                      # Example implementations
│   ├── SampleWysiwygField.js
│   ├── SampleButtonClickField.js
│   ├── SampleRatingField.js
│   └── index.js
└── index.js                       # Main exports

🎯 Field Categories

Matomo FormAnalytics supports three field categories:

| Category | Description | Examples | |----------|-------------|----------| | TEXT | Text-based input fields | password, text, url, tel, email, search, textarea | | SELECTABLE | Selection-based fields | color, date, datetime, datetime-local, month, number, range, time, week, select | | CHECKABLE | Checkbox/radio fields | radio, checkbox |

📚 Sample Implementations

The package includes sample implementations in the examples/ folder that demonstrate proper usage of the cleanup functionality:

SampleWysiwygField

  • Category: TEXT
  • Purpose: Handles rich text editing with ProseMirror editor
  • Selector: .formulate-input-element--wysiwyg[data-name]
  • Cleanup: Uses default BaseField event listeners (automatically tracked)

SampleButtonClickField

  • Category: SELECTABLE
  • Purpose: Tracks button clicks and counts them as field interactions
  • Selector: .custom-button[data-name]
  • Cleanup: Uses _addTrackedEventListener() and _trackTimer() for proper cleanup

SampleRatingField

  • Category: SELECTABLE
  • Purpose: Handles star rating elements with click-based selection
  • Selector: .formulate-input-element--rating-container[data-name]
  • Cleanup: Uses _addTrackedEventListener() and _trackTimer() for proper cleanup

All sample implementations now include proper memory management and cleanup patterns that prevent memory leaks.

🚀 Build Formats

ES Modules

import FormAnalyticsCustomFieldTracker from '@doghouse/matomo-form-analytics-custom-field-tracker';

CommonJS

const FormAnalyticsCustomFieldTracker = require('@doghouse/matomo-form-analytics-custom-field-tracker');

Browser (UMD)

<script src="https://unpkg.com/@doghouse/matomo-form-analytics-custom-field-tracker/dist/index.umd.js"></script>
<script>
  MatomoFormAnalyticsCustomFieldTracker.init();
</script>

🧪 Testing

# Run tests
npm test

# Run tests with coverage
npm run test:coverage

# Run tests in watch mode
npm run test:watch

📦 Building

# Build all formats
npm run build

# Build specific format
npm run build:esm
npm run build:cjs
npm run build:umd

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature-name
  3. Make your changes
  4. Add tests for new functionality
  5. Run tests: npm test
  6. Commit your changes: git commit -am 'Add feature'
  7. Push to the branch: git push origin feature-name
  8. Submit a pull request

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

  • Built for Matomo FormAnalytics integration
  • Inspired by modern JavaScript patterns and best practices
  • Community feedback and contributions

📞 Support


Made with ❤️ by Doghouse Agency