Skip to content

Support APPMAP_CUSTOMER_ID (managed entitlement) - #2391

Open
dividedmind wants to merge 2 commits into
mainfrom
feat/customer-id
Open

Support APPMAP_CUSTOMER_ID (managed entitlement)#2391
dividedmind wants to merge 2 commits into
mainfrom
feat/customer-id

Conversation

@dividedmind

Copy link
Copy Markdown
Collaborator

Motivation

In enterprise deployments where licensing is already settled by a B2B agreement, requiring every user to authenticate against getappmap.com is pure friction: it fails outright under network restrictions and creates audit review burden for a flow that grants nothing the contract hasn't already granted. Administrators can now set a customer ID through the IDE's organization-configuration channels, which passes it down to CLI subprocesses as APPMAP_CUSTOMER_ID.

This is the appmap-js half of that feature, and it is deliberately small — until now the CLI simply ignored the variable. Two things change: telemetry reports it, and checkLicense treats it as entitlement.

It is not a cryptographic license key and must never be described as an enforcement mechanism. The code is open source, so any client-side check is a business-process boundary rather than a security boundary. The identifier is not a secret and grants nothing on its own.

Companion PRs, which share the naming and the semantics:

common.customerid telemetry

buildDefaultConfiguration reads APPMAP_CUSTOMER_ID and adds common.customerid alongside the other common.* defaults, so it lands on every event, on both backends, from every entry point. Blank and whitespace-only values read as unset rather than being reported as an empty string, and base.properties / APPMAP_TELEMETRY_PROPERTIES can still override it.

Note this lives in @appland/telemetry, so it applies to every consumer of the package rather than only to the CLI. The CLI picks it up for free — packages/cli/src/cli.ts:51 configures the shared default client.

The property name is deliberately all lower-case with no separator, matching the existing common.extname / common.ideversion style. The other two implementations depend on that spelling.

Entitlement in checkLicense

A customer ID entitles the installation on its own, so the licensing check reports that state instead of warning about a key the user was never expected to have.

Entitlement is not a credential and never substitutes for one. When an API key is present it is still checked — the real key wins for authentication, and the check stays visible to the server, which usage tracking depends on. Entitlement only decides whether a bad outcome is fatal: a key that is missing, rejected or unverifiable is still reported, but does not fail a required check for an installation a contract already entitles.

Entitlement is reported unconditionally whenever a customer ID is set, before anything is said about the key. An entitled installation that stayed silent about it would be indistinguishable from one where the customer ID was never configured — exactly the thing an administrator is trying to confirm.

What the command prints:

State Output
Entitled, no API key Entitled to AppMap as customer acme-corp at machine id <id>.
Entitled, valid key the same line, then Valid license for alice at machine id <id>.
Entitled, key rejected or unverifiable the same line, then the warning
Not entitled unchanged in every case

The machine id is on the entitlement message for the same reason 1acab71 put it on the valid-key message — correlating telemetry with user logs for troubleshooting. That applies at least as strongly here, since attributing usage to a customer is the whole point of this deployment mode.

Incidental fixes

Found while restructuring checkLicense; none are related to customer IDs.

  • The invalid-key throw sat inside the try that catches check failures, so a required check against an invalid key reported Failed to check license key over its own error before rethrowing it.
  • A failed check warn()ed the caught error object, printing a backtrace nobody needs. It now reports the underlying message on the one line.
  • A fatal problem was both warned about and thrown, reporting it twice once the caller surfaced the error. licenseProblem now does one or the other.

Notes for review

checkLicense switches from the imported warn to the global console.warn behind a file-level eslint-disable no-console. This is the one stylistic oddity in the diff and it is deliberate: most of this package imports warn from 'console', which sidesteps the lint rule because the rule only matches the global — but that import also resolves against Node's real console rather than the one jest installs, so the messages escape test buffering and land on raw stderr, uncaptured. Writing to the global lets the tests spy on it with a plain jest.spyOn(console, 'warn') and lets TEST_SILENT=false attribute each message to its source line. A shared output helper owning that suppression would be better than either, but that is a package-wide change (~53 files) and not one for this PR to make.

One contract change: the error thrown for a missing key is now the warning text rather than 'License key is required', so what you see is what gets thrown. All three call sites use required = false, so nothing observable changes today.

Testing

packages/cli/tests/unit/lib/checkLicense.spec.ts is new — 13 cases covering entitled and unentitled across no key, valid key, rejected key and unreachable check, each split into the warns-when-not-required and throws-without-warning-when-required outcomes, plus blank-value handling and the both-present case that pins "the key is still checked".

Three cases added to packages/telemetry/tests/client.spec.ts for present, absent and blank customer IDs.

yarn verify is clean on both packages.

Managed deployments identify a customer through a B2B agreement rather
than an authenticated session. The IDE extension passes that identifier
down to subprocesses as APPMAP_CUSTOMER_ID; report it on every event so
usage can be attributed without a signed-in user.

The property sits with the other common.* defaults in
buildDefaultConfiguration, so it applies to both backends and to every
event, and base.properties / APPMAP_TELEMETRY_PROPERTIES can still
override it. Blank and whitespace-only values read as unset rather than
being reported as an empty string.

The identifier is not a credential and grants nothing on its own.

Assisted-by: Claude:claude-opus-5[1m]
In a managed deployment, entitlement is settled by a B2B agreement and
the subprocess receives APPMAP_CUSTOMER_ID instead of an API key. Report
that state explicitly rather than warning about a missing license key,
which is cosmetic noise for a user who was never expected to sign in.

Entitlement is not a credential and never substitutes for one. When an
API key is present it is still checked, so the real key wins for
authentication and the check stays visible to the server, which usage
tracking depends on. Entitlement only decides whether a bad outcome is
fatal: a key that is missing, rejected or unverifiable is still
reported, but does not fail a `required` check for an installation a
contract already entitles. Blank values read as unset.

Entitlement is reported whenever a customer ID is set, whatever the key
turns out to be, since an entitled installation that says nothing about
it cannot be told apart from one where the ID was never configured. The
message carries the telemetry machine id, as the valid-key message has
since 1acab71: the rationale given there - correlating telemetry with
user logs for troubleshooting - applies at least as strongly here.

Restructuring for that also fixes three warts. The invalid-key throw
used to sit inside the try block that catches check failures, so a
required check against an invalid key reported "Failed to check license
key" over its own error before rethrowing it. A failed check warn()ed
the error object itself, printing a backtrace nobody needs; it now
reports the underlying message on the one line. And a fatal problem was
both warned about and thrown, reporting it twice once the caller
surfaced the error; licenseProblem now does one or the other.

checkLicense switches from the imported `warn` to the global
console.warn behind an eslint-disable. The import resolves against
Node's real console, which is not the one jest installs, so the messages
escape test buffering; writing to the global lets the new tests spy on
it and lets TEST_SILENT=false attribute each message to its source line.

Assisted-by: Claude:claude-opus-5[1m]
@dividedmind
dividedmind requested review from kgilpin and a lite review from Copilot August 24, 2026 12:40
@dividedmind dividedmind self-assigned this Aug 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for managed-entitlement via APPMAP_CUSTOMER_ID across the telemetry and CLI layers, so enterprise installations can be attributed (telemetry) and treated as entitled (CLI checkLicense) without requiring authentication against getappmap.com.

Changes:

  • Add common.customerid to telemetry default properties when APPMAP_CUSTOMER_ID is set to a non-blank value.
  • Update CLI license checking to report entitlement when a customer ID is present and to make missing/rejected/unverifiable keys non-fatal for entitled installs.
  • Add unit tests covering customer ID telemetry behavior and the new checkLicense entitlement matrix.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/telemetry/tests/client.spec.ts Adds coverage to ensure common.customerid is omitted/trimmed/treated as unset appropriately.
packages/telemetry/src/client.ts Reads and trims APPMAP_CUSTOMER_ID, conditionally emitting common.customerid in default telemetry properties.
packages/telemetry/README.md Documents APPMAP_CUSTOMER_ID behavior and clarifies it is attribution-only (not a credential).
packages/cli/tests/unit/lib/checkLicense.spec.ts New tests covering entitled/unentitled states across missing/valid/invalid/unreachable key checks and required vs optional behavior.
packages/cli/src/lib/customerId.ts Introduces a small helper to read/trim APPMAP_CUSTOMER_ID and treat blank as unset.
packages/cli/src/lib/checkLicense.ts Implements entitlement reporting and adjusts failure handling to be non-fatal when entitled, while still checking API keys when present.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants