Skip to content

About

AEM extension that tracks component usage across all published pages. Identifies dead/unused components, shows usage counts per resource type, and exports reports as CSV/JSON. Built with React Spectrum + Adobe I/O Runtime. Deployed via AEM Extension Manager (Cloud Service).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

AEM Component Usage Analytics

An Adobe App Builder extension for AEM as a Cloud Service that reports which components are actually used across published pages, and which component definitions in /apps are unused (dead).

  • Frontend — React 18 + React Spectrum (@react-spectrum/*)
  • Backend — Adobe I/O Runtime serverless action (nodejs18)
  • Data — AEM Query Builder API (/bin/querybuilder.json) with facet aggregation
  • Auth — Adobe IMS OAuth Server-to-Server (client_credentials) → AEM bearer token
  • Hosting — Experience Cloud Shell extension (dx/excshell/1) surfaced via the AEM Extension Manager

Scope note. The report counts page-level sling:resourceType values, i.e. the components rendering each published page. It does not enumerate every embedded component instance inside page trees. Counting instance-level usage requires walking each jcr:content subtree and is significantly more expensive on large sites.


Project structure

.
├── app.config.yaml                  # Extension + Runtime action registration
├── package.json
├── actions/
│   └── queryComponents.js           # I/O Runtime action (IMS auth, QueryBuilder, aggregation)
├── src/
│   ├── index.html                   # SPA entry (referenced by app.config.yaml `impl`)
│   ├── index.js                     # React root
│   ├── App.js                       # Fetches from the action, renders the report
│   ├── webpack.config.js            # Injects AIO_RUNTIME_NAMESPACE at build time
│   └── components/
│       ├── UsageTable.js            # React Spectrum Table + dead-components notice
│       ├── DetailView.js            # Row drill-down, pages grouped by site/section
│       └── ExportButton.js          # CSV / JSON export
├── .aio                             # Generated by `aio app use` — do not edit
└── .env                             # Local secrets — do not commit

Prerequisites

Requirement Notes
Node.js 18+ Matches engines.node and the nodejs18 action runtime
Adobe I/O CLI v8+ v8 is required for app.config.yaml support; legacy .aio/manifest.yaml projects cannot use this layout
Adobe Developer Console project With the App Builder entitlement
AEM as a Cloud Service environment Author tier reachable over HTTPS from I/O Runtime
Adobe IMS Org Admin role To create OAuth Server-to-Server credentials and a technical account

Install the CLI:

npm install -g @adobe/aio-cli
aio login
aio --version   # confirm 8.x or newer

Configuration parameters

All variables live in .env locally and are injected into the action as inputs via app.config.yaml. Never commit .env or .aio.

Variable Required Default Purpose
AEM_AUTHOR_BASE_URL recommended — AEM author URL, e.g. https://author-p1234-e5678.adobeaemcloud.com. Queried by default because publication status lives on author.
AEM_BASE_URL fallback — Used when AEM_AUTHOR_BASE_URL is unset.
AEM_CONTENT_PATH no /content Root scope for the usage query. A caller may narrow this, but cannot widen it.
AEM_APPS_PATH no /apps Root scanned for component definitions when detecting dead components.
IMS_CLIENT_ID yes — OAuth Server-to-Server client ID from Developer Console. Also sent as X-Api-Key.
IMS_CLIENT_SECRET yes — OAuth Server-to-Server client secret.
IMS_ORG_ID yes — IMS Organization ID.
IMS_SCOPES yes — Comma-separated scopes. See below.
IMS_TECHNICAL_ACCOUNT_ID yes — Technical account ID associated with the credential.
AIO_RUNTIME_NAMESPACE auto — Generated by the CLI. Injected into the web bundle at build time by webpack DefinePlugin; must not be hand-edited.

Typical IMS scopes

openid,AdobeID,read_organizations,additional_info.projectedProductContext

Confirm the exact scope list shown on the OAuth Server-to-Server credential page in your Developer Console project — Adobe documents a fixed set rather than granular per-endpoint scopes for AEM access.

Example .env

AEM_AUTHOR_BASE_URL=https://author-p1234-e5678.adobeaemcloud.com
AEM_CONTENT_PATH=/content
AEM_APPS_PATH=/apps

IMS_CLIENT_ID=
IMS_CLIENT_SECRET=
IMS_ORG_ID=
IMS_TECHNICAL_ACCOUNT_ID=
IMS_SCOPES=openid,AdobeID,read_organizations,additional_info.projectedProductContext

# auto-generated — do not edit
# AIO_runtime_namespace=
# AIO_runtime_auth=
# AIO_runtime_apihost=

AEM permissions

The Runtime action authenticates as an IMS technical account, not as the signed-in author. That account needs repository read access:

Path Required privilege Used for
/content/** jcr:read Usage counts, sample pages, last-modified
/apps/** jcr:read Dead-component detection

/apps access is commonly not granted by default. If it is missing, the report still renders usage counts but the dead-components section reports status: "forbidden" in the response and shows an actionable message in the UI rather than silently appearing empty.


Local development

npm install
aio login
aio app use -w Stage
aio app run

aio app run starts the Runtime action locally and proxies the SPA. Open the URL printed by the CLI. The action is reached through the dev proxy at /api/v1/web/default/queryComponents, which is why App.js falls back to a relative URL when AIO_RUNTIME_NAMESPACE is not injected.

Verifying the action in isolation

# Requires a valid IMS access token: the action sets require-adobe-auth: true.
curl.exe -s -X POST "http://localhost:8443/api/v1/web/default/queryComponents" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $IMS_ACCESS_TOKEN" \
  -d '{"includeDead":true}'

Get the token via OAuth Playground using your Developer Console OAuth Server-to-Server credentials, then export it as IMS_ACCESS_TOKEN. Without the header the action returns 401 before reaching AEM.

Inspect deadComponentsStatus to confirm the /apps scan actually ran:

status Meaning Action
ok definitions read, scan complete —
forbidden 403 on /apps grant jcr:read on /apps/**
unavailable other HTTP or transport failure check reason
unparsed nodes returned but no usable path key log a raw hit to inspect the key
partial some definitions unparseable usually mixed hit writers
ok + truncated: true hit the guessTotal cap raise the cap or narrow AEM_APPS_PATH

Also check countsFromFacets. If it is false, usage counts were derived from the paged page query rather than facet aggregation and totals are lower bounds.


Deploy to Stage

aio app use -w Stage
aio app deploy

Stage deployments are isolated and do not appear in the AEM Extension Manager. Use Stage to validate the action, IAM configuration, and QueryBuilder behaviour.


Deploy to Production

aio app use -w Production
aio app deploy

Deploying to the Production workspace makes the extension visible to Adobe App Registry. For third-party extensions, submit it for approval:

  1. Adobe Developer Console → your project → the extension → Production workspace.
  2. Submit for approval.
  3. Adobe Exchange → Manage → Apps pending review → accept.
  4. Approval injects the extension into all AEM as a Cloud Service author services in the organization.

Extensions deployed only to Stage are never injected into AEM. Production deployment plus approval is a prerequisite for the Extension Manager to list anything.


Enabling in the AEM Extension Manager

  1. In AEM Author, open Tools → Extension Manager. (https://<author-host>/ui#/aem/libs/granite/extensionmanager/content.html — exact path varies by release; the Tools menu entry is authoritative.)
  2. Select your Program and Environment.
  3. Find AEM Component Usage Analytics in the listing. It appears once the Production deployment has been approved.
  4. Toggle it on for that environment. Enable/disable is per-instance, so Stage and Production AEM environments are controlled independently.
  5. Open the extension to render the report.

Roles required

Role Can
System Administrator Enable/disable extensions on production environments; configure parameters
Developer Add BYO extensions, preview, generate shareable preview links

Preview before enabling. Use the preview icon in the extension listing to open the app in a preproduction environment without installing it for the whole environment.

Restricting to specific environments. Any approved extension is offered across all AEM programs and environments in the organization. To self-restrict, inspect the repo query parameter at registration time and skip registration when it does not match your target host.


How the report is produced

  1. App.js POSTs { path?, includeDead } to the Runtime web action, forwarding the caller's IMS token from the window.dx context (the action sets require-adobe-auth: true).
  2. The action exchanges IMS client credentials for an AEM bearer token via @adobe/aio-lib-ims and calls GET {AEM_AUTHOR_BASE_URL}/bin/querybuilder.json.
  3. Three queries run in parallel:
    • Facet counts — p.facets=true over published pages, bucketed by jcr:content/sling:resourceType. This is the GROUP BY; QueryBuilder has no SQL-style aggregation.
    • Page details — selective hits carrying resource type, cq:lastModified, cq:lastModifiedBy, jcr:title, used for last-modified, author, and sample pages.
    • Component definitions — type=cq:Component under /apps. Definitions carry no sling:resourceType; the type is derived from the definition node's own path. Used components are subtracted to yield dead components.
  4. A caller-supplied path is validated to be /content or a descendant, so the technical account cannot be steered outside the content root.
  5. The action returns { scope, generatedAt, countsFromFacets, components[], deadComponents[], deadComponentsStatus }.

Known limitations

  • Author tier, not publish. Publication status is only available on author. Querying the publish tier would require the Dispatcher to allow /bin/querybuilder.json, which the default publish filters block.
  • Page-level usage. Embedded component instances are not counted individually (see scope note above).
  • Facet cap. Facet bucket counts are limited per query. If a site exceeds the cap, counts are incomplete and countsFromFacets should be verified.
  • Result caps. The page query and definition query use p.guessTotal and will truncate on very large repositories; pageQueryTruncated and deadComponentsStatus.truncated surface this.
  • Token storage. @adobe/aio-lib-ims persists tokens to App Builder State, which has usage limits that a busy report page can exhaust. Adobe's current guidance is to use @adobe/aio-lib-core-auth for Runtime token generation instead; this is a contained change to the getAemToken function only.
  • Index coverage. Facet performance depends on sling:resourceType being indexed under the content path. Verify with the AEM Query Performance tool (/ui#/aem/libs/granite/operations/content/diagnosistools/queryPerformance.html) before trusting counts on a large repository.

About

AEM extension that tracks component usage across all published pages. Identifies dead/unused components, shows usage counts per resource type, and exports reports as CSV/JSON. Built with React Spectrum + Adobe I/O Runtime. Deployed via AEM Extension Manager (Cloud Service).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages