Skip to content

Commit af5bea4

Browse files
committed
Add deployment environments to events
1 parent 92c4599 commit af5bea4

13 files changed

Lines changed: 118 additions & 9 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ 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`. See [configuration.md](references/configuration.md).
13+
1214
## Official Docs
1315

1416
Primary docs:

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@ import { Exceptionless } from "@exceptionless/browser";
2323
await Exceptionless.startup((config) => {
2424
config.apiKey = "API_KEY_HERE";
2525
config.version = "1.2.3";
26+
config.environment = "production";
2627
config.setUserIdentity("12345678", "Blake");
2728
config.defaultTags.push("Example", "JavaScript");
28-
config.defaultData["deployment"] = { environment: "production" };
2929
});
3030
```
3131

32+
## Deployment environment
33+
34+
Set `config.environment = "production"` or call `config.setEnvironment("production")`. Per-event `setEnvironment("staging")` overrides the default. Names are trimmed and lowercased; empty names, names longer than 64 characters, and control characters are ignored. Missing values remain unspecified. 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.
35+
3236
## Privacy
3337

3438
For deeper guidance on PII removal, read [data-exclusions.md](data-exclusions.md).

.agents/skills/exceptionless-javascript/references/sending-events.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ Use the platform package's `Exceptionless` singleton unless the user is building
99

1010
## Common Events
1111

12+
Deployment environments can be overridden per event:
13+
14+
```js
15+
await Exceptionless.createLog("Deployment complete").setEnvironment("staging").submit();
16+
```
17+
18+
Without an override, the event uses `config.environment`. Repeated errors from different environments are queued separately; the server still groups the same error into one stack.
19+
1220
```js
1321
import { Exceptionless } from "@exceptionless/browser";
1422

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ The definition of the word exceptionless is: to be without exception. Exceptionl
88

99
## Browser
1010

11+
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, lowercased, and limited to 64 characters. Missing or invalid names remain unspecified. The top-level `environment` is separate from machine/runtime diagnostics in `data.@environment`; stacks and fixed versions remain shared across environments.
12+
1113
You can install the npm package via `npm install @exceptionless/browser --save`
1214
or via cdn [`https://unpkg.com/@exceptionless/browser`](https://unpkg.com/@exceptionless/browser).
1315
Next, you just need to call startup during your app's startup to automatically

packages/core/src/EventBuilder.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,20 @@ import { Event, EventType, KnownEventDataKeys } from "./models/Event.js";
33
import { ManualStackingInfo } from "./models/data/ManualStackingInfo.js";
44
import { UserInfo } from "./models/data/UserInfo.js";
55
import { EventContext } from "./models/EventContext.js";
6-
import { isEmpty, stringify } from "./Utils.js";
6+
import { isEmpty, normalizeEnvironment, stringify } from "./Utils.js";
77
import { EventPluginContext } from "./plugins/EventPluginContext.js";
88

99
export class EventBuilder {
10+
public setEnvironment(value: string | null | undefined): EventBuilder {
11+
const environment = normalizeEnvironment(value);
12+
if (environment) {
13+
this.target.environment = environment;
14+
} else {
15+
delete this.target.environment;
16+
}
17+
return this;
18+
}
19+
1020
public target: Event;
1121
public client: ExceptionlessClient;
1222
public context: EventContext;

packages/core/src/Utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,3 +559,11 @@ export function allowProcessToExitWithoutWaitingForTimerOrInterval(timeoutOrInte
559559
(timeoutOrIntervalId as { unref: () => ReturnType<typeof setTimeout> }).unref();
560560
}
561561
}
562+
export function normalizeEnvironment(value: string | null | undefined): string | undefined {
563+
if (typeof value !== "string") {
564+
return undefined;
565+
}
566+
const name = value.trim();
567+
// eslint-disable-next-line no-control-regex -- Deployment names cannot contain control characters.
568+
return name && name.length <= 64 && !/[\u0000-\u001f\u007f-\u009f]/u.test(name) ? name.toLowerCase() : undefined;
569+
}

packages/core/src/configuration/Configuration.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,29 @@ import { DefaultEventQueue } from "../queue/DefaultEventQueue.js";
1313
import { IEventQueue } from "../queue/IEventQueue.js";
1414
import { ISubmissionClient } from "../submission/ISubmissionClient.js";
1515
import { DefaultSubmissionClient } from "../submission/DefaultSubmissionClient.js";
16-
import { guid } from "../Utils.js";
16+
import { guid, normalizeEnvironment } from "../Utils.js";
1717
import { KnownEventDataKeys } from "../models/Event.js";
1818
import { InMemoryStorage } from "../storage/InMemoryStorage.js";
1919
import { IStorage } from "../storage/IStorage.js";
2020
import { LocalStorage } from "../storage/LocalStorage.js";
2121
import { ServerSettings } from "../configuration/SettingsManager.js";
2222

2323
export class Configuration {
24+
private _environment: string | undefined;
25+
26+
/** The default deployment environment for every event. */
27+
public get environment(): string | undefined {
28+
return this._environment;
29+
}
30+
31+
public set environment(value: string | null | undefined) {
32+
this._environment = normalizeEnvironment(value);
33+
}
34+
35+
public setEnvironment(value: string | null | undefined): void {
36+
this.environment = value;
37+
}
38+
2439
constructor() {
2540
this.services = {
2641
lastReferenceIdManager: new DefaultLastReferenceIdManager(),

packages/core/src/models/Event.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { ManualStackingInfo } from "../models/data/ManualStackingInfo.js";
88
export type EventType = "error" | "usage" | "log" | "404" | "session" | string;
99

1010
export interface Event {
11+
/** The deployment environment, such as production or staging. */
12+
environment?: string;
1113
/** The event type (ie. error, log message, feature usage). */
1214
type?: EventType;
1315
/** The event source (ie. machine name, log name, feature name). */

packages/core/src/plugins/default/ConfigurationDefaultsPlugin.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isEmpty, stringify } from "../../Utils.js";
1+
import { isEmpty, normalizeEnvironment, stringify } from "../../Utils.js";
22
import { EventPluginContext } from "../../plugins/EventPluginContext.js";
33
import { IEventPlugin } from "../../plugins/IEventPlugin.js";
44

@@ -9,6 +9,12 @@ export class ConfigurationDefaultsPlugin implements IEventPlugin {
99
public run(context: EventPluginContext): Promise<void> {
1010
const { dataExclusions, defaultData, defaultTags } = context.client.config;
1111
const ev = context.event;
12+
const environment = normalizeEnvironment(ev.environment ?? context.client.config.environment);
13+
if (environment) {
14+
ev.environment = environment;
15+
} else {
16+
delete ev.environment;
17+
}
1218

1319
if (defaultTags) {
1420
ev.tags = [...(ev.tags || []), ...defaultTags];

packages/core/src/plugins/default/DuplicateCheckerPlugin.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,27 +50,31 @@ export class DuplicateCheckerPlugin implements IEventPlugin {
5050

5151
const error = context.event.data?.[KnownEventDataKeys.Error];
5252
const hashCode = calculateHashCode(error);
53+
const environment = context.event.environment;
5354
if (hashCode) {
5455
const count = context.event.count || 1;
5556
const now = this._getCurrentTime();
5657

57-
const merged = this._mergedEvents.filter((s) => s.hashCode === hashCode)[0];
58+
const merged = this._mergedEvents.find((s) => s.hashCode === hashCode && s.environment === environment);
5859
if (merged) {
5960
merged.incrementCount(count);
6061
merged.updateDate(context.event.date);
6162
context.log.info("Ignoring duplicate event with hash: " + hashCode);
6263
context.cancelled = true;
6364
}
6465

65-
if (!context.cancelled && this._processedHashCodes.some((h) => h.hash === hashCode && h.timestamp >= now - this._interval)) {
66+
if (
67+
!context.cancelled &&
68+
this._processedHashCodes.some((h) => h.hash === hashCode && h.environment === environment && h.timestamp >= now - this._interval)
69+
) {
6670
context.log.trace("Adding event with hash: " + hashCode);
6771
this._mergedEvents.push(new MergedEvent(hashCode, context, count));
6872
context.cancelled = true;
6973
}
7074

7175
if (!context.cancelled) {
7276
context.log.trace(`Enqueueing event with hash: ${hashCode} to cache`);
73-
this._processedHashCodes.push({ hash: hashCode, timestamp: now });
77+
this._processedHashCodes.push({ hash: hashCode, environment, timestamp: now });
7478

7579
// Only keep the last 50 recent errors.
7680
while (this._processedHashCodes.length > 50) {
@@ -91,16 +95,19 @@ export class DuplicateCheckerPlugin implements IEventPlugin {
9195

9296
interface TimestampedHash {
9397
hash: number;
98+
environment: string | undefined;
9499
timestamp: number;
95100
}
96101

97102
class MergedEvent {
98103
public hashCode: number;
104+
public readonly environment: string | undefined;
99105
private _count: number;
100106
private _context: EventPluginContext;
101107

102108
constructor(hashCode: number, context: EventPluginContext, count: number) {
103109
this.hashCode = hashCode;
110+
this.environment = context.event.environment;
104111
this._context = context;
105112
this._count = count;
106113
}

0 commit comments

Comments
 (0)