Skip to content

Commit acf7215

Browse files
ejsmithclaude
andcommitted
Restrict environment names to a strict identifier
Names are trimmed, then must match ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$. Anything else is unspecified and never rewritten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f7b9ca2 commit acf7215

8 files changed

Lines changed: 74 additions & 16 deletions

File tree

‎.agents/skills/exceptionless-javascript/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Use this skill to produce source-accurate setup code, integration guidance, and
99

1010
Keep answers compact. Prefer pointing to official docs for broad product behavior, and use local package READMEs/source to correct stale snippets or repo-specific package details.
1111

12-
Deployment environments use `config.environment` or `config.setEnvironment(name)` as the default, and `builder.setEnvironment(name)` for overrides. They serialize as top-level `environment`, separately from `data.@environment`, preserving the supplied casing after trimming. See [configuration.md](references/configuration.md).
12+
Deployment environments use `config.environment` or `config.setEnvironment(name)` as the default, and `builder.setEnvironment(name)` for overrides. They serialize as top-level `environment`, separately from `data.@environment`, preserving the supplied casing after trimming; names must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` or stay unspecified. See [configuration.md](references/configuration.md).
1313

1414
## Pick References
1515

‎.agents/skills/exceptionless-javascript/references/configuration.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ await Exceptionless.startup((config) => {
3131

3232
## Deployment environment
3333

34-
Set `config.environment = "production"` or call `config.setEnvironment("production")`. Per-event `setEnvironment("staging")` overrides the default. Names are trimmed and retain their supplied casing; empty names, names longer than 64 characters, and names with control characters are unspecified. An invalid per-event name stays unspecified instead of using the default; pass `null` or `undefined` to `setEnvironment` to use the default. Missing values remain unspecified. The server filters case-insensitively and normalizes aggregation keys. This property is independent of `data.@environment` runtime metadata and of the application version. It does not change server stack grouping or create per-environment status.
34+
Set `config.environment = "production"` or call `config.setEnvironment("production")`. Per-event `setEnvironment("staging")` overrides the default. Names are trimmed and retain their supplied casing, then must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$` (ASCII letters, digits, `.`, `_` and `-`, starting with a letter or digit, at most 64 characters). Anything else, such as `Staging West`, `prod/eu`, `produção`, empty or over-long names, is unspecified and never rewritten. An invalid per-event name stays unspecified instead of using the default; pass `null` or `undefined` to `setEnvironment` to use the default. Missing values remain unspecified. The server filters case-insensitively and normalizes aggregation keys. This property is independent of `data.@environment` runtime metadata and of the application version. It does not change server stack grouping or create per-environment status.
3535

3636
## Privacy
3737

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ more information on configuring the client.
158158

159159
#### Deployment environments
160160

161-
Set a deployment environment in startup configuration with `config.environment = "production"` (or `config.setEnvironment("production")`). Override it on an event with `Exceptionless.createLog("Example").setEnvironment("staging").submit()`. Names are trimmed and limited to 64 characters, preserving the supplied casing. The server filters case-insensitively and normalizes aggregation keys. Missing or invalid names remain unspecified, and an invalid per-event name does not fall back to the configured default; pass `null` or `undefined` to `setEnvironment` to use the default. The top-level `environment` is separate from machine/runtime diagnostics in `data.@environment`; stacks and fixed versions remain shared across environments.
161+
Set a deployment environment in startup configuration with `config.environment = "production"` (or `config.setEnvironment("production")`). Override it on an event with `Exceptionless.createLog("Example").setEnvironment("staging").submit()`. Names are trimmed, then must match `^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`: ASCII letters, digits, `.`, `_` and `-`, starting with a letter or digit, at most 64 characters. Casing is preserved and names are never rewritten, so values such as `Staging West`, `prod/eu` or `produção` are invalid. The server filters case-insensitively and normalizes aggregation keys. Missing or invalid names remain unspecified, and an invalid per-event name does not fall back to the configured default; pass `null` or `undefined` to `setEnvironment` to use the default. The top-level `environment` is separate from machine/runtime diagnostics in `data.@environment`; stacks and fixed versions remain shared across environments.
162162

163163
### Submitting Events and Errors
164164

‎packages/core/src/Utils.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,13 +561,18 @@ export function allowProcessToExitWithoutWaitingForTimerOrInterval(timeoutOrInte
561561
(timeoutOrIntervalId as { unref: () => ReturnType<typeof setTimeout> }).unref();
562562
}
563563
}
564+
const validEnvironment = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
565+
566+
/**
567+
* Trims the name and returns it with its casing preserved, or undefined when it is not a valid
568+
* deployment environment name: ASCII letters, digits, `.`, `_` and `-`, starting with a letter or digit, at most 64 characters.
569+
*/
564570
export function normalizeEnvironment(value: string | null | undefined): string | undefined {
565571
if (typeof value !== "string") {
566572
return undefined;
567573
}
568574
const name = value.trim();
569-
// eslint-disable-next-line no-control-regex -- Deployment names cannot contain control characters.
570-
return name && name.length <= 64 && !/[\u0000-\u001f\u007f-\u009f]/u.test(name) ? name : undefined;
575+
return validEnvironment.test(name) ? name : undefined;
571576
}
572577

573578
/**

‎packages/core/test/ExceptionlessClient.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ describe("ExceptionlessClient", () => {
3939

4040
test.each([
4141
[" QA\t", "QA"],
42-
["x".repeat(65), undefined]
42+
["x".repeat(65), undefined],
43+
["Staging West", undefined],
44+
["prod/eu", undefined]
4345
])("should normalize environment %j assigned by a later plugin", async (environment, expected) => {
4446
const client = new ExceptionlessClient();
4547
client.config.apiKey = "UNIT_TEST_API_KEY";

‎packages/core/test/Utils.test.ts‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test, vi } from "vitest";
22

3-
import { endsWith, guid, isEmpty, isMatch, parseVersion, prune, randomNumber, startsWith, stringify, toBoolean } from "../src/Utils.js";
3+
import { endsWith, guid, isEmpty, isMatch, normalizeEnvironment, parseVersion, prune, randomNumber, startsWith, stringify, toBoolean } from "../src/Utils.js";
44

55
describe("Utils", () => {
66
function getObjectWithInheritedProperties(): unknown {
@@ -1237,4 +1237,48 @@ describe("Utils", () => {
12371237
expect(toBoolean(true)).toBe(true);
12381238
});
12391239
});
1240+
1241+
describe("normalizeEnvironment", () => {
1242+
test.each([
1243+
["production", "production"],
1244+
["Staging-EU", "Staging-EU"],
1245+
["qa.2", "qa.2"],
1246+
["dev_local", "dev_local"],
1247+
["a", "a"],
1248+
["0", "0"],
1249+
[" Production \t", "Production"],
1250+
["\u00a0Production\u2003", "Production"],
1251+
["a" + "b".repeat(63), "a" + "b".repeat(63)],
1252+
[" " + "a".repeat(64) + " ", "a".repeat(64)]
1253+
])("should accept %j", (value, expected) => {
1254+
expect(normalizeEnvironment(value)).toBe(expected);
1255+
});
1256+
1257+
test.each([
1258+
null,
1259+
undefined,
1260+
"",
1261+
" ",
1262+
" \t\r\n ",
1263+
"Staging West",
1264+
"prod/eu",
1265+
"prod:eu",
1266+
"-prod",
1267+
".prod",
1268+
"_prod",
1269+
"produção",
1270+
"İ".repeat(64),
1271+
"a".repeat(65),
1272+
" " + "a".repeat(65) + " ",
1273+
"prod\ninvalid",
1274+
"prod\tinvalid",
1275+
"pro\u0000d",
1276+
"prod\u007f",
1277+
"pro\u0085d",
1278+
"prod\u200b",
1279+
"prod\n\u2028x"
1280+
])("should reject %j", (value) => {
1281+
expect(normalizeEnvironment(value)).toBeUndefined();
1282+
});
1283+
});
12401284
});

‎packages/core/test/configuration/Configuration.test.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@ describe("Configuration", () => {
1010
expect(config.environment).toBe("Production");
1111
config.environment = "Staging";
1212
expect(config.environment).toBe("Staging");
13-
config.environment = "İ".repeat(64);
14-
expect(config.environment).toBe("İ".repeat(64));
13+
config.environment = "a".repeat(64);
14+
expect(config.environment).toBe("a".repeat(64));
1515
config.setEnvironment("");
1616
expect(config.environment).toBeUndefined();
1717
config.setEnvironment("x".repeat(65));
1818
expect(config.environment).toBeUndefined();
1919
config.setEnvironment("prod\ninvalid");
2020
expect(config.environment).toBeUndefined();
21+
config.setEnvironment("Staging-EU");
22+
expect(config.environment).toBe("Staging-EU");
23+
config.setEnvironment("Staging West");
24+
expect(config.environment).toBeUndefined();
2125
config.setEnvironment("\tProduction\n");
2226
expect(config.environment).toBe("Production");
2327
});

‎packages/core/test/plugins/default/ConfigurationDefaultsPlugin.test.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,16 @@ describe("ConfigurationDefaultsPlugin", () => {
3131
expect(JSON.parse(JSON.stringify(builder.target)).environment).toBe("Staging");
3232
});
3333

34-
test.each(["", " ", "x".repeat(65), "prod\ninvalid"])("should keep invalid override %j unspecified", async (environment) => {
35-
const client = new ExceptionlessClient();
36-
client.config.environment = "production";
37-
const builder = client.createLog("test", "message").setEnvironment(environment);
38-
await new ConfigurationDefaultsPlugin().run(new EventPluginContext(client, builder.target, new EventContext()));
39-
expect(builder.target.environment).toBeUndefined();
40-
});
34+
test.each(["", " ", "x".repeat(65), "prod\ninvalid", "Staging West", "prod/eu", "prod:eu", "-prod", ".prod", "produção"])(
35+
"should keep invalid override %j unspecified",
36+
async (environment) => {
37+
const client = new ExceptionlessClient();
38+
client.config.environment = "production";
39+
const builder = client.createLog("test", "message").setEnvironment(environment);
40+
await new ConfigurationDefaultsPlugin().run(new EventPluginContext(client, builder.target, new EventContext()));
41+
expect(builder.target.environment).toBeUndefined();
42+
}
43+
);
4144

4245
test.each([null, undefined])("should apply the default when the override is cleared with %j", async (environment) => {
4346
const client = new ExceptionlessClient();

0 commit comments

Comments
 (0)