@noctcore/eslint-plugin-observability 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -601,7 +601,7 @@ var rules = {
601
601
 
602
602
  // src/index.ts
603
603
  var NAMESPACE = "noctcore-observability";
604
- var VERSION = "0.2.0";
604
+ var VERSION = "0.3.2";
605
605
  var plugin = {
606
606
  meta: { name: "@noctcore/eslint-plugin-observability", version: VERSION },
607
607
  rules,
package/dist/index.js CHANGED
@@ -573,7 +573,7 @@ var rules = {
573
573
 
574
574
  // src/index.ts
575
575
  var NAMESPACE = "noctcore-observability";
576
- var VERSION = "0.2.0";
576
+ var VERSION = "0.3.2";
577
577
  var plugin = {
578
578
  meta: { name: "@noctcore/eslint-plugin-observability", version: VERSION },
579
579
  rules,
@@ -83,7 +83,7 @@ The purge job needs the registry at run time and this rule needs it at lint time
83
83
  in code both can import, and pass it into the ESLint config. Never retype it into the config: two
84
84
  hand-kept copies of one list drift.
85
85
 
86
- ```ts
86
+ ```ts prose reason="the registry module the config imports, not a lint example"
87
87
  // packages/shared/src/audit/pii-registry.ts
88
88
  /** Audit payload keys holding personal data. The user purge anonymizes these. */
89
89
  export const AUDIT_PII_FIELDS = ['newEmail', 'email'] as const;
@@ -111,28 +111,33 @@ export default [
111
111
 
112
112
  ## Examples
113
113
 
114
- ```ts
115
- // Bad: raw email in the payload, and no declaration that the purger must scrub it
114
+ ```ts bad reports=4 options={"auditCallees":["auditService.log","auditService.logOrThrow"]}
115
+ // raw email in the payload, and no declaration that the purger must scrub it
116
116
  await this.auditService.log({
117
117
  action: 'auth.email_change.requested',
118
118
  userId,
119
119
  metadata: { newEmail: normalizedEmail },
120
120
  });
121
121
 
122
- // Bad: before/after snapshots of a PII column
122
+ // before/after snapshots of a PII column
123
123
  await this.auditService.log({
124
124
  action: 'auth.email_changed',
125
125
  before: { email: previousEmail },
126
126
  after: { email: pending.newEmail },
127
127
  });
128
128
 
129
- // Bad: the rule cannot see what this bag carries
129
+ // the rule cannot see what this bag carries
130
130
  await this.auditService.log({ action, metadata: { ...grant.auditMetadata } });
131
+ ```
132
+
133
+ Declaring the keys is the fix, so this example runs with `registeredFields: ['newEmail', 'email']`:
131
134
 
132
- // Good, with registeredFields: ['newEmail', 'email']
135
+ ```ts good reconfigured options={"auditCallees":["auditService.log","auditService.logOrThrow"],"registeredFields":["newEmail","email"]}
133
136
  await this.auditService.log({ action, userId, metadata: { newEmail: normalizedEmail } });
137
+ ```
134
138
 
135
- // Good: nothing PII-shaped; `emailSent` is a flag about the data
139
+ ```ts good options={"auditCallees":["auditService.log","auditService.logOrThrow"]}
140
+ // nothing PII-shaped; `emailSent` is a flag about the data
136
141
  await this.auditService.log({ action, metadata: { role, outcome, emailSent } });
137
142
  ```
138
143
 
@@ -8,13 +8,15 @@ A caught error carries a **stack trace** and often a **`cause`** — the parts y
8
8
  debug a production failure. A catch block that logs only `e.message`, `String(e)`, or `` `${e}` ``
9
9
  throws that away: the log records _that_ something failed but not _where_ or _why_.
10
10
 
11
- ```ts
12
- // stack and cause are gone
11
+ ```ts bad
12
+ // stack and cause are gone
13
13
  try { await run(); } catch (e) {
14
14
  logger.error(`run failed: ${e.message}`);
15
15
  }
16
+ ```
16
17
 
17
- // ✓ the whole error survives
18
+ ```ts good
19
+ // the whole error survives
18
20
  try { await run(); } catch (e) {
19
21
  logger.error('run failed', { err: e });
20
22
  }
@@ -32,12 +34,12 @@ A `catch (e)` block where **all** of the following hold:
32
34
  If the error is passed whole anywhere (`logger.error('x', e)`, `{ err: e }`), or `.stack` / `.cause` /
33
35
  any other property is read, or it is re-thrown, its diagnostics survive and the rule stays silent:
34
36
 
35
- ```ts
36
- // .stack is read not a lossy form
37
- catch (e) { logger.error(`failed: ${e.stack}`); }
37
+ ```ts good
38
+ // .stack is read: not a lossy form
39
+ try { await run(); } catch (e) { logger.error(`failed: ${e.stack}`); }
38
40
 
39
- // mixed use the full capture wins
40
- catch (e) { logger.error(`${e.message}`, { err: e }); }
41
+ // mixed use: the full capture wins
42
+ try { await run(); } catch (e) { logger.error(`${e.message}`, { err: e }); }
41
43
  ```
42
44
 
43
45
  Distinct from a fully-**unused** catch binding (`catch (e) { cleanup(); }`), which this rule
@@ -10,7 +10,7 @@ request by months. This rule catches the most common shape — a variable, prope
10
10
  whose **name** matches a sensitive-field denylist appearing inside a logger call.
11
11
 
12
12
  It reads **names, never values**, so it is a heuristic. It still ships at `error`: a miss puts a
13
- credential in a long-lived log sink, while a false positive costs a rename or an explicit `redact()`.
13
+ credential in a long-lived log sink, while a false positive costs a rename.
14
14
  Tests that log a secret on purpose (to prove a redaction boundary works) should turn the rule off for
15
15
  those files in config.
16
16
 
@@ -19,16 +19,24 @@ those files in config.
19
19
  Inside a logger call (`<logger>.<method>(...)`), any identifier, member-access property, or object key
20
20
  whose name matches the denylist:
21
21
 
22
- ```ts
23
- // all three flag
22
+ ```ts bad reports=3
23
+ // all three flag
24
24
  logger.info('login', { password });
25
25
  logger.error('auth failed', { userPassword: pw });
26
26
  logger.info('session', user.token);
27
+ ```
27
28
 
28
- // ✓ redact first
29
- logger.info('login', { password: redact(password) });
29
+ ```ts good
30
+ // log who, not the credential
31
+ logger.info('login', { userId });
32
+ logger.error('auth failed', { userId, reason: 'bad-credentials' });
33
+ logger.info('session', { sessionId: user.sessionId });
30
34
  ```
31
35
 
36
+ Wrapping the value in a call does not help: `{ password: redact(password) }` is still reported,
37
+ twice, because the key and the argument are both named `password`. The rule has no notion of a
38
+ redaction helper. Omit the field and log an identifier for the subject instead.
39
+
32
40
  Matching is **name-segment aware**. A single-word denyName (`token`) matches a camelCase or
33
41
  snake_case segment (`accessToken`, `access_token`) but **not** a longer word that merely contains it
34
42
  (`tokenize`, `tokenizer`). A multi-word denyName (`apiKey`) matches the compacted name
@@ -37,14 +45,14 @@ snake_case segment (`accessToken`, `access_token`) but **not** a longer word tha
37
45
  String **literals** are never inspected — only names — so a message that mentions a sensitive word is
38
46
  fine:
39
47
 
40
- ```ts
41
- // a literal, not a value
48
+ ```ts good
49
+ // a literal, not a value
42
50
  logger.info('password reset email sent');
43
51
  ```
44
52
 
45
53
  ## Options
46
54
 
47
- ```ts
55
+ ```ts prose reason="the options type, not a lint example"
48
56
  type Options = {
49
57
  /**
50
58
  * Field names to treat as sensitive (case-insensitive, segment-aware).
@@ -6,7 +6,7 @@
6
6
 
7
7
  Dynamic values baked into a log **message** string are unqueryable. When you write
8
8
 
9
- ```ts
9
+ ```ts bad
10
10
  logger.info(`processing task ${taskId} for ${userId}`);
11
11
  ```
12
12
 
@@ -14,7 +14,7 @@ a log aggregator stores one opaque line of free text — it cannot index, filter
14
14
  `taskId` or `userId`, because they are fused into the message. The value belongs in a structured
15
15
  context object, where each field stays a first-class, queryable attribute:
16
16
 
17
- ```ts
17
+ ```ts good
18
18
  logger.info('processing task', { taskId, userId });
19
19
  ```
20
20
 
@@ -24,11 +24,13 @@ A logger call — `<logger>.<method>(...)` where `<method>` is `info` / `warn` /
24
24
  `<logger>` is a configured logger name — that receives a **template literal with expressions** as a
25
25
  **direct** positional argument.
26
26
 
27
- ```ts
28
- // dynamic values interpolated into the message
27
+ ```ts bad
28
+ // dynamic values interpolated into the message
29
29
  logger.error(`failed: ${err.code}`);
30
+ ```
30
31
 
31
- // ✓ static message + structured context
32
+ ```ts good
33
+ // static message + structured context
32
34
  logger.error('request failed', { code: err.code });
33
35
  ```
34
36
 
@@ -38,8 +40,8 @@ namespace or `this` (`this.logger.info(...)`, `app.log.warn(...)`) are recognise
38
40
  Only **direct** arguments are inspected. A template literal nested inside a context object is building
39
41
  a value, not the message, and is never flagged:
40
42
 
41
- ```ts
42
- // the template builds a URL field, not the message
43
+ ```ts good
44
+ // the template builds a URL field, not the message
43
45
  logger.info('fetching', { url: `${base}/tasks` });
44
46
  ```
45
47
 
@@ -51,7 +53,7 @@ A template with **no** expressions carries no dynamic value and is ignored
51
53
 
52
54
  ## Options
53
55
 
54
- ```ts
56
+ ```ts prose reason="the options type, not a lint example"
55
57
  type Options = {
56
58
  /** Logger object names to scan. Default: ['console', 'logger', 'log']. */
57
59
  loggers?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noctcore/eslint-plugin-observability",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Structured-logging discipline ESLint rules — context objects over interpolated messages, no sensitive fields in logs, no error-detail loss.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -46,7 +46,7 @@
46
46
  "test": "vitest run"
47
47
  },
48
48
  "dependencies": {
49
- "@noctcore/eslint-utils": "^0.1.0",
49
+ "@noctcore/eslint-utils": "^0.1.1",
50
50
  "@typescript-eslint/utils": "^8.61.1"
51
51
  },
52
52
  "peerDependencies": {