Repository: dbader/node-datadog-metrics
Pull request #118 (2024-06-22 01:42:03)

Add more documentation about API vs. App keys
Issue #115 demonstrated how easy it is to get confused about API vs. application keys, so I’ve added some more documentation here about the difference as well as links to the official documentation about it (https://docs.datadoghq.com/account_management/api-app-keys/).

I’ve also added a custom `AuthorizationError` class with a more explanatory message (also with links to the docs), so people should see something more helpful in their console or logs, too. If you log the full object, you’ll see something like:

```
AuthorizationError: Your Datadog API key is not authorized to send metrics. Check to make sure the DATADOG_API_KEY environment variable or the `apiKey` option is set to a valid API key for your Datadog account, and that it is not an *application* key. For more, see: https://docs.datadoghq.com/account_management/api-app-keys/
    at /Users/rbrackett/Dev/node-datadog-metrics/lib/reporters.js:96:29
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  code: 403,
  [cause]: ApiException [Error]: HTTP-Code: 403
  Message: {"errors":["Forbidden"],"additionalProperties":{"status":"error","code":403,"statuspage":"http://status.datadoghq.com","twitter":"http://twitter.com/datadogops","email":"support@datadoghq.com"}}
      at MetricsApiResponseProcessor.<anonymous> (/Users/rbrackett/Dev/node-datadog-metrics/node_modules/@datadog/datadog-api-client/dist/packages/datadog-api-client-v1/apis/MetricsApi.js:458:23)
      at Generator.next (<anonymous>)
      at fulfilled (/Users/rbrackett/Dev/node-datadog-metrics/node_modules/@datadog/datadog-api-client/dist/packages/datadog-api-client-v1/apis/MetricsApi.js:5:58)
      at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
    code: 403,
    body: APIErrorResponse {
      errors: [Array],
      additionalProperties: [Object]
    }
  }
}
```

I asked on Datadog’s Slack to see if there was any known way of differentiating between API and Application keys (so we could have a special case that tells someone they’ve mixed them up), but never got any responses. 🤷

---

As a side note, I kind of think we should remove the `appKey` option altogether. There are things it could be useful for (e.g. some fancy possibilities in #33), it doesn’t actually get used for anything right now, so I imagine it mostly just adds confusion.

Mr0grog: FYI: I'll merge this tomorrow morning (June 24) U.S. Pacific time if there's no feedback.

Patch:
diff --git a/README.md b/README.md
index 8c1ba8c..e2da68f 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,7 @@ There's also a longer [tutorial](https://dbader.org/blog/monitoring-your-nodejs-
 ### Datadog API key
 
 Make sure the `DATADOG_API_KEY` environment variable is set to your Datadog
-API key. You can find the API key under [Integrations > APIs](https://app.datadoghq.com/account/settings#api). *You only need to provide the API key, not the APP key. However, you can provide an APP key if you want by setting the `DATADOG_APP_KEY` environment variable.*
+API key (you can also set it via the `apiKey` option in code). You can find the API key under [Integrations > APIs](https://app.datadoghq.com/account/settings#api). *Please note the API key is different from an **application key**. For more details, see [Datadog’s “API and Application Keys” docs](https://docs.datadoghq.com/account_management/api-app-keys/).*
 
 ### Module setup
 
@@ -124,9 +124,16 @@ Where `options` is an object and can contain the following:
 * `apiKey`: Sets the Datadog API key. (optional)
     * It's usually best to keep this in an environment variable.
       Datadog-metrics looks for the API key in `DATADOG_API_KEY` by default.
-* `appKey`: Sets the Datadog APP key. (optional)
-    * It's usually best to keep this in an environment variable.
-      Datadog-metrics looks for the APP key in `DATADOG_APP_KEY` by default.
+    * You must either set this option or the environment variable. An API key
+      is required to send metrics.
+    * Make sure not to confuse this with your _application_ key! For more
+      details, see: https://docs.datadoghq.com/account_management/api-app-keys/
+* `appKey`: Sets the Datadog application key. (optional)
+    * It's usually best to keep this in an environment variable. Datadog-metrics
+      looks for the application key in `DATADOG_APP_KEY` by default.
+    * This is different from the API key (see above), which is required. For
+      more about the different between API and application keys, see:
+      https://docs.datadoghq.com/account_management/api-app-keys/
 * `defaultTags`: Default tags used for all metric reporting. (optional)
     * Set tags that are common to all metrics.
 * `onError`: A function to call when there are asynchronous errors seding
@@ -302,6 +309,8 @@ npm test
 
     * Fix types and documentation for the `aggregates` option for histograms and the `histogram.aggregates` option for the library as a whole. It was previously listed as `aggregations`, which was incorrect. (Thanks to @Calyhre in #117.)
 
+    * Improve documentation and add a more detailed error message about API keys vs. application keys.
+
     [View diff](https://github.com/dbader/node-datadog-metrics/compare/v0.11.1...main)
 
 * 0.11.1 (2023-09-28)
diff --git a/lib/errors.js b/lib/errors.js
new file mode 100644
index 0000000..6ab2d09
--- /dev/null
+++ b/lib/errors.js
@@ -0,0 +1,27 @@
+'use strict';
+
+/**
+ * Represents an authorization failure response from the Datadog API, usually
+ * because of an invalid API key.
+ *
+ * @property {'DATADOG_AUTHORIZATION_ERROR'} code
+ * @property {number} status
+ */
+class AuthorizationError extends Error {
+    /**
+     * Create an `AuthorizationError`.
+     * @param {string} message
+     * @param {object} [options]
+     * @param {Error} [options.cause]
+     */
+    constructor(message, options = {}) {
+        // @ts-expect-error the ECMAScript version we target with TypeScript
+        // does not include `error.cause` (new in ES 2022), but all versions of
+        // Node.js we support do.
+        super(message, { cause: options.cause });
+        this.code = 'DATADOG_AUTHORIZATION_ERROR';
+        this.status = 403;
+    }
+}
+
+module.exports = { AuthorizationError };
diff --git a/lib/reporters.js b/lib/reporters.js
index 330d513..8d6b195 100644
--- a/lib/reporters.js
+++ b/lib/reporters.js
@@ -1,5 +1,6 @@
 'use strict';
 const datadogApiClient = require('@datadog/datadog-api-client');
+const { AuthorizationError } = require('./errors');
 const { logDebug, logDeprecation, logError } = require('./logging');
 
 /**
@@ -91,6 +92,17 @@ class DatadogReporter {
                 }
             })
             .catch((error) => {
+                if (error.code === 403) {
+                    error = new AuthorizationError(
+                        'Your Datadog API key is not authorized to send ' +
+                        'metrics. Check to make sure the DATADOG_API_KEY ' +
+                        'environment variable or the `apiKey` option is set ' +
+                        'to a valid API key for your Datadog account, and ' +
+                        'that it is not an *application* key. For more, see: ' +
+                        'https://docs.datadoghq.com/account_management/api-app-keys/',
+                        { cause: error }
+                    );
+                }
                 if (typeof onError === 'function') {
                     onError(error);
                 } else {


Test patch:
diff --git a/test/loggers_tests.js b/test/loggers_tests.js
index f5b1779..ab8acde 100644
--- a/test/loggers_tests.js
+++ b/test/loggers_tests.js
@@ -8,6 +8,7 @@ const nock = require('nock');
 chai.should();
 const loggers = require('../lib/loggers');
 const reporters = require('../lib/reporters');
+const { AuthorizationError } = require('../lib/errors');
 const BufferedMetricsLogger = loggers.BufferedMetricsLogger;
 
 describe('BufferedMetricsLogger', function() {
@@ -276,6 +277,26 @@ describe('BufferedMetricsLogger', function() {
         }, 50);
     });
 
+    it('should use a special error for bad API keys', function(done) {
+        nock('https://api.datadoghq.com')
+            .post('/api/v1/series')
+            .reply(403, { errors: ['Forbidden'] });
+
+        const logger = new BufferedMetricsLogger({ apiKey: 'not-valid' });
+        logger.gauge('test.gauge', 23);
+
+        logger.flush(
+            () => done(new Error('The success handler was called!')),
+            (error) => {
+                if (error instanceof AuthorizationError) {
+                    done();
+                } else {
+                    done(new Error(`The error was not an AuthorizationError: ${error}`));
+                }
+            }
+        );
+    });
+
     it('should allow two instances to use different credentials', function(done) {
         const apiKeys = ['abc', 'xyz'];
         let receivedKeys = [];