diff --git a/acquia_telemetry.info.yml b/acquia_telemetry.info.yml
index 34408c4..680e100 100644
--- a/acquia_telemetry.info.yml
+++ b/acquia_telemetry.info.yml
@@ -2,7 +2,7 @@ name: 'Acquia Telemetry'
 core: 8.x
 core_version_requirement: '^8 || ^9'
 type: module
-package: Lightning
+package: Acquia
 description: "By installing the Acquia Telemetry module, you agree to allow it to send anonymized telemetry data to Acquia for the purposes and on the conditions set forth below. If you do not agree, do not install this software.
 
 The Acquia Telemetry module sends anonymized data about Acquia product usage to Acquia for security, operations management, statistical analyses, and for research and development purposes. Specifically, it sends the following data:<br />
diff --git a/acquia_telemetry.services.yml b/acquia_telemetry.services.yml
index 37ebfec..1ba1ea9 100644
--- a/acquia_telemetry.services.yml
+++ b/acquia_telemetry.services.yml
@@ -1,4 +1,19 @@
 services:
   acquia.telemetry:
     class: Drupal\acquia_telemetry\Telemetry
-    arguments: ['@extension.list.module', '@http_client', '@config.factory', '@state', '@app.root']
+    arguments: ['@http_client', '@config.factory', '@state', '@event_dispatcher']
+  event_dispatcher.acquia_telemetry:
+    public: false
+    class: 'Drupal\acquia_telemetry\TelemetryDecorator'
+    decorates: event_dispatcher
+    arguments: ['@event_dispatcher.inner']
+    # Workaround for serializing decorated services. Remove when either
+    # https://www.drupal.org/project/drupal/issues/2896993 or
+    # https://www.drupal.org/project/drupal/issues/2531564 are fixed.
+    properties:
+      _serviceId: event_dispatcher
+  acquia_telemetry.custom_telemetry:
+    class: 'Drupal\acquia_telemetry\BasicTelemetrySubscriber'
+    arguments: ['@extension.list.module']
+    tags:
+      - { name: event_subscriber }
diff --git a/src/BasicTelemetrySubscriber.php b/src/BasicTelemetrySubscriber.php
new file mode 100644
index 0000000..da72ab2
--- /dev/null
+++ b/src/BasicTelemetrySubscriber.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Drupal\acquia_telemetry;
+
+use Drupal\Core\Extension\ModuleExtensionList;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Defines an event subscriber for providing basic telemetry data.
+ *
+ * This subscriber provides the following data to Amplitude:
+ * - The PHP version
+ * - The Drupal version
+ * - A list of all available Acquia or Lightning modules, along with their
+ *   version and a string indicating if they are installed or not.
+ */
+final class BasicTelemetrySubscriber implements EventSubscriberInterface {
+
+  /**
+   * The module list service.
+   *
+   * @var \Drupal\Core\Extension\ModuleExtensionList
+   */
+  private $moduleList;
+
+  /**
+   * CustomTelemetrySubscriber constructor.
+   *
+   * @param \Drupal\Core\Extension\ModuleExtensionList $module_list
+   *   The module list service.
+   */
+  public function __construct(ModuleExtensionList $module_list) {
+    $this->moduleList = $module_list;
+  }
+
+  /**
+   * Collects custom telemetry.
+   */
+  public function collectTelemetry($event) {
+    $event
+      ->set('php.version', phpversion())
+      ->set('drupal.version', \Drupal::VERSION)
+      ->set('extensions', $this->getExtensionInfo());
+  }
+
+  /**
+   * Get an array of information about Acquia extensions.
+   *
+   * @return array
+   *   An array of extension info keyed by the extensions machine name. E.g.,
+   *   ['lightning_layout' => ['version' => '8.2.0', 'status' => 'enabled']].
+   */
+  private function getExtensionInfo() {
+    $acquia_extensions = array_filter($this->moduleList->getAllAvailableInfo(), [$this, 'isAcquiaExtension'], ARRAY_FILTER_USE_KEY);
+    $extension_info = [];
+
+    foreach ($acquia_extensions as $name => $extension) {
+      // Version is unset for dev versions. In order to generate reports, we
+      // need some value for version, even if it is just the major version.
+      $version = isset($extension['version']) ? $extension['version'] : $extension['core'];
+      $extension_info[$name]['version'] = $version;
+    }
+
+    $installed_modules = $this->moduleList->getAllInstalledInfo();
+    foreach ($acquia_extensions as $name => $extension) {
+      $extension_info[$name]['status'] = array_key_exists($name, $installed_modules) ? 'enabled' : 'disabled';
+    }
+
+    return $extension_info;
+  }
+
+  /**
+   * Determines if an extension is an Acquia extension, by name.
+   *
+   * @param string $name
+   *   The extension name.
+   *
+   * @return bool
+   *   TRUE if the extension is an Acquia extension name, FALSE otherwise.
+   */
+  private function isAcquiaExtension($name) {
+    return strpos($name, 'acquia_') === 0 || strpos($name, 'lightning_') === 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    return [
+      TelemetryEvents::COLLECT => 'collectTelemetry',
+    ];
+  }
+
+}
diff --git a/src/CollectTelemetryEvent.php b/src/CollectTelemetryEvent.php
new file mode 100644
index 0000000..d4be463
--- /dev/null
+++ b/src/CollectTelemetryEvent.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\acquia_telemetry;
+
+use Drupal\Component\Utility\NestedArray;
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Defines an event to collect telemetry information about Acquia extensions.
+ */
+final class CollectTelemetryEvent extends Event {
+
+  /**
+   * The collected telemetry payload to send to Amplitude.
+   *
+   * @var array
+   */
+  private $properties = [];
+
+  /**
+   * CollectTelemetryEvent constructor.
+   *
+   * @param array $properties
+   *   (optional) Initial data for the telemetry payload.
+   */
+  public function __construct(array $properties = []) {
+    $this->properties = $properties;
+  }
+
+  /**
+   * Adds a value to the payload.
+   *
+   * @param string|array $key
+   *   The key in the properties array. Can be a string, with nesting levels
+   *   denoted with periods (like `php.version`), or an array of nested keys
+   *   (like `['php', 'version']`).
+   * @param mixed $value
+   *   The value to insert into the payload.
+   */
+  public function set($key, $value) {
+    if (is_string($key)) {
+      $key = explode('.', trim($key));
+    }
+    NestedArray::setValue($this->properties, $key, $value);
+  }
+
+  /**
+   * Returns the collected telemetry data.
+   *
+   * @return array
+   *   The collected telemetry data.
+   */
+  public function getAll() {
+    return $this->properties;
+  }
+
+}
diff --git a/src/Telemetry.php b/src/Telemetry.php
index d04e8e7..07065c5 100644
--- a/src/Telemetry.php
+++ b/src/Telemetry.php
@@ -4,17 +4,15 @@ namespace Drupal\acquia_telemetry;
 
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\Crypt;
-use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\Extension\ModuleExtensionList;
 use Drupal\Core\State\StateInterface;
-use Drupal\lightning_core\ComponentDiscovery;
 use GuzzleHttp\ClientInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 
 /**
- * Telemetry service.
+ * Defines a service to send telemetry data to Amplitude.
  */
-class Telemetry {
+final class Telemetry {
 
   /**
    * Amplitude API URL.
@@ -35,13 +33,6 @@ class Telemetry {
    */
   private $apiKey = 'f32aacddde42ad34f5a3078a621f37a9';
 
-  /**
-   * The extension.list.module service.
-   *
-   * @var \Drupal\Core\Extension\ModuleExtensionList
-   */
-  private $moduleList;
-
   /**
    * The HTTP client.
    *
@@ -64,32 +55,29 @@ class Telemetry {
   private $state;
 
   /**
-   * The application root directory.
+   * The event dispatcher service.
    *
-   * @var string
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
    */
-  private $root;
+  private $eventDispatcher;
 
   /**
    * Constructs a telemetry object.
    *
-   * @param \Drupal\Core\Extension\ModuleExtensionList $module_list
-   *   The extension.list.module service.
    * @param \GuzzleHttp\ClientInterface $http_client
    *   The HTTP client.
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The config.factory service.
    * @param \Drupal\Core\State\StateInterface $state
    *   The state service.
-   * @param string $app_root
-   *   The Drupal application root.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher service.
    */
-  public function __construct(ModuleExtensionList $module_list, ClientInterface $http_client, ConfigFactoryInterface $config_factory, StateInterface $state, $app_root) {
-    $this->moduleList = $module_list;
+  public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, StateInterface $state, EventDispatcherInterface $event_dispatcher) {
     $this->httpClient = $http_client;
     $this->configFactory = $config_factory;
     $this->state = $state;
-    $this->root = $app_root;
+    $this->eventDispatcher = $event_dispatcher;
   }
 
   /**
@@ -149,33 +137,6 @@ class Telemetry {
     }
   }
 
-  /**
-   * Get an array of information about Lightning extensions.
-   *
-   * @return array
-   *   An array of extension info keyed by the extensions machine name. E.g.,
-   *   ['lightning_layout' => ['version' => '8.2.0', 'status' => 'enabled']].
-   */
-  private function getExtensionInfo() {
-    $all_modules = $this->moduleList->getAllAvailableInfo();
-    $acquia_extensions = array_intersect_key($all_modules, array_flip($this->getAcquiaExtensionNames()));
-    $extension_info = [];
-
-    foreach ($acquia_extensions as $name => $extension) {
-      // Version is unset for dev versions. In order to generate reports, we
-      // need some value for version, even if it is just the major version.
-      $version = isset($extension['version']) ? $extension['version'] : $extension['core'];
-      $extension_info[$name]['version'] = $version;
-    }
-
-    $installed_modules = $this->moduleList->getAllInstalledInfo();
-    foreach ($acquia_extensions as $name => $extension) {
-      $extension_info[$name]['status'] = array_key_exists($name, $installed_modules) ? 'enabled' : 'disabled';
-    }
-
-    return $extension_info;
-  }
-
   /**
    * Creates an Amplitude event.
    *
@@ -188,20 +149,13 @@ class Telemetry {
    *   An Amplitude event with basic info already populated.
    */
   private function createEvent($type, array $properties) {
-    $default_properties = [
-      'extensions' => $this->getExtensionInfo(),
-      'php' => [
-        'version' => phpversion(),
-      ],
-      'drupal' => [
-        'version' => \Drupal::VERSION,
-      ],
-    ];
+    $event = new CollectTelemetryEvent();
+    $this->eventDispatcher->dispatch(TelemetryEvents::COLLECT, $event);
 
     return [
       'event_type' => $type,
       'user_id' => $this->getUserId(),
-      'event_properties' => NestedArray::mergeDeep($default_properties, $properties),
+      'event_properties' => $event->getAll(),
     ];
   }
 
@@ -215,15 +169,4 @@ class Telemetry {
     return Crypt::hashBase64($this->configFactory->get('system.site')->get('uuid'));
   }
 
-  /**
-   * Gets an array of all Acquia Drupal extensions.
-   *
-   * @return array
-   *   A flat array of all Acquia Drupal extensions.
-   */
-  public function getAcquiaExtensionNames() {
-    $discovery = new ComponentDiscovery($this->root);
-    return array_keys($discovery->getAll());
-  }
-
 }
diff --git a/src/TelemetryDecorator.php b/src/TelemetryDecorator.php
new file mode 100644
index 0000000..f6534b7
--- /dev/null
+++ b/src/TelemetryDecorator.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Drupal\acquia_telemetry;
+
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Decorates the event dispatcher to filter custom telemetry subscribers.
+ */
+final class TelemetryDecorator implements EventDispatcherInterface {
+
+  /**
+   * The event subscribers which are allowed to send telemetry.
+   *
+   * @var string[]
+   */
+  private $allowedProviders = [
+    BasicTelemetrySubscriber::class,
+  ];
+
+  /**
+   * The decorated event dispatcher service.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  private $decorated;
+
+  /**
+   * CustomTelemetryDecorator constructor.
+   *
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $decorated
+   *   The decorated event dispatcher service.
+   */
+  public function __construct(EventDispatcherInterface $decorated) {
+    $this->decorated = $decorated;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addListener($eventName, $listener, $priority = 0) {
+    return $this->decorated->addListener($eventName, $listener, $priority);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addSubscriber(EventSubscriberInterface $subscriber) {
+    if ($this->isAllowedSubscriber($subscriber)) {
+      return $this->decorated->addSubscriber($subscriber);
+    }
+    else {
+      $message = sprintf('%s is not allowed to send telemetry.', get_class($subscriber));
+      throw new \DomainException($message);
+    }
+  }
+
+  /**
+   * Checks if an event subscriber is allowed to send custom telemetry.
+   *
+   * @param \Symfony\Component\EventDispatcher\EventSubscriberInterface $subscriber
+   *   The event subscriber to check.
+   *
+   * @return bool
+   *   TRUE if the subscriber is allowed to send custom telemetry, or does not
+   *   subscribe to that event; FALSE otherwise.
+   */
+  private function isAllowedSubscriber(EventSubscriberInterface $subscriber) {
+    $class = get_class($subscriber);
+
+    if (array_key_exists(TelemetryEvents::COLLECT, $class::getSubscribedEvents())) {
+      return in_array($class, $this->allowedProviders, TRUE);
+    }
+    else {
+      return TRUE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeListener($eventName, $listener) {
+    return $this->decorated->removeListener($eventName, $listener);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeSubscriber(EventSubscriberInterface $subscriber) {
+    return $this->decorated->removeSubscriber($subscriber);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getListeners($eventName = NULL) {
+    return $this->decorated->getListeners($eventName);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getListenerPriority($eventName, $listener) {
+    return $this->decorated->getListenerPriority($eventName, $listener);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasListeners($eventName = NULL) {
+    return $this->decorated->hasListeners($eventName);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function dispatch($event) {
+    return $this->decorated->dispatch($event);
+  }
+
+}
diff --git a/src/TelemetryEvents.php b/src/TelemetryEvents.php
new file mode 100644
index 0000000..2e26990
--- /dev/null
+++ b/src/TelemetryEvents.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\acquia_telemetry;
+
+/**
+ * Defines the available telemetry-related events.
+ */
+final class TelemetryEvents {
+
+  /**
+   * The name of the event fired when collecting telemetry data for Amplitude.
+   * Event subscribers receive an instance of
+   * \Drupal\acquia_telemetry\CollectTelemetryEvent and can add any arbitrary
+   * data to it.
+   *
+   * To prevent malicious or erroneous code from sending garbage to Amplitude,
+   * event subscribers wishing to handle this event must be explicitly listed
+   * in \Drupal\acquia_telemetry\CustomTelemetryDecorator::$allowedProviders,
+   * which is deliberately hard-coded and can only be changed by a module
+   * maintainer.
+   *
+   * @see \Drupal\acquia_telemetry\CollectTelemetryEvent
+   *
+   * @var string
+   *
+   * @Event
+   */
+  const COLLECT = 'acquia_telemetry.collect';
+
+}
