@posthog/plugin-utils 1.1.2 → 1.2.0

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/LICENSE CHANGED
@@ -327,12 +327,12 @@ SOFTWARE.
327
327
 
328
328
  ---
329
329
 
330
- Some files in this codebase contain code from MCPCat/mcpcat-typescript-sdk.
330
+ Some files in this codebase contain code from agentcathq/agentcat-typescript-sdk (formerly MCPCat/mcpcat-typescript-sdk).
331
331
  In such cases it is explicitly stated in the file header. This license only applies to the relevant code in such cases.
332
332
 
333
333
  MIT License
334
334
 
335
- Copyright (c) 2025 MCPcat
335
+ Copyright (c) 2025 AgentCat, Inc. (formerly MCPcat)
336
336
 
337
337
  Permission is hereby granted, free of charge, to any person obtaining a copy
338
338
  of this software and associated documentation files (the "Software"), to deal
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Returns the minified chunk-id IIFE the SDK reads at runtime to map an
3
+ * error's stack back to a chunk id. With `releaseId`, the snippet also pins
4
+ * `_posthogReleaseId` on the global so the SDK emits the release on every
5
+ * exception (first write wins, so the first loaded chunk decides).
6
+ *
7
+ * Byte-for-byte contract with posthog-cli (`CODE_SNIPPET_TEMPLATE` and
8
+ * `CODE_SNIPPET_WITH_RELEASE_TEMPLATE` in cli/src/sourcemaps/constant.rs): the
9
+ * CLI recognizes and removes previously injected snippets by exact substring
10
+ * match, so any change here must ship in the CLI constants too. Ids are
11
+ * JSON-encoded for the same reason the CLI encodes them: an id carrying a quote
12
+ * would otherwise break out of the string literal. Covered by a parity test in
13
+ * chunk-ids.spec.ts.
14
+ */
15
+ export declare function createChunkIdSnippet(chunkId: string, releaseId?: string): string;
16
+ /**
17
+ * Returns the comment posthog-cli discovers chunk ids from at upload time
18
+ * (`CHUNKID_COMMENT_PREFIX` in cli/src/sourcemaps/constant.rs), including the
19
+ * leading newline the CLI's exact-match removal expects.
20
+ */
21
+ export declare function createChunkIdComment(chunkId: string): string;
22
+ /**
23
+ * Mints a fresh random chunk id: a new id per chunk, per build.
24
+ */
25
+ export declare function createChunkId(): string;
26
+ /**
27
+ * Derives a chunk id from the chunk's own bytes (UUIDv5), so identical code keeps its id across
28
+ * rebuilds and re-uploads dedupe against the symbol set already stored. Used in event release
29
+ * mode, where the release travels inside the chunk instead of being bound to the symbol set, so
30
+ * a per-build random id would create a fresh symbol set on every build.
31
+ *
32
+ * The id is content-addressed over what `renderChunk` sees. posthog-cli derives its own ids from
33
+ * the file on disk, which by then also carries the trailing `sourceMappingURL` comment, so the
34
+ * same chunk gets a different (but equally stable) id depending on which tool injected it.
35
+ */
36
+ export declare function createStableChunkId(code: string): string;
37
+ /**
38
+ * Extracts the chunk id from source that already carries one, using the same
39
+ * detection the CLI uses: a `//# chunkId=` comment at the start of a line.
40
+ * The injected IIFE is deliberately not matched — its marker text can appear
41
+ * inside string literals of bundled code (docs strings, bundled PostHog
42
+ * tooling), and the CLI only ever reads the line-anchored comment. Keeping the
43
+ * two in sync guarantees this returning an id ⟺ the CLI finding one.
44
+ */
45
+ export declare function determineChunkIdFromSource(code: string): string | undefined;
46
+ //# sourceMappingURL=chunk-ids.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-ids.d.ts","sourceRoot":"","sources":["../src/chunk-ids.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAMhF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAOD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAUxD;AASD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAa3E"}
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
14
+ (()=>{
15
+ __webpack_require__.r = (exports1)=>{
16
+ if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
+ value: 'Module'
18
+ });
19
+ Object.defineProperty(exports1, '__esModule', {
20
+ value: true
21
+ });
22
+ };
23
+ })();
24
+ var __webpack_exports__ = {};
25
+ __webpack_require__.r(__webpack_exports__);
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ createStableChunkId: ()=>createStableChunkId,
28
+ createChunkIdSnippet: ()=>createChunkIdSnippet,
29
+ createChunkId: ()=>createChunkId,
30
+ createChunkIdComment: ()=>createChunkIdComment,
31
+ determineChunkIdFromSource: ()=>determineChunkIdFromSource
32
+ });
33
+ const external_crypto_namespaceObject = require("crypto");
34
+ function createChunkIdSnippet(chunkId, releaseId) {
35
+ const chunk = JSON.stringify(chunkId);
36
+ if (void 0 === releaseId) return `!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._posthogChunkIds=e._posthogChunkIds||{},e._posthogChunkIds[n]=${chunk})}catch(e){}}();`;
37
+ return `!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e._posthogReleaseId=e._posthogReleaseId||${JSON.stringify(releaseId)};var n=(new e.Error).stack;n&&(e._posthogChunkIds=e._posthogChunkIds||{},e._posthogChunkIds[n]=${chunk})}catch(e){}}();`;
38
+ }
39
+ function createChunkIdComment(chunkId) {
40
+ return `\n//# chunkId=${chunkId}`;
41
+ }
42
+ function createChunkId() {
43
+ return external_crypto_namespaceObject.randomUUID();
44
+ }
45
+ const CHUNK_ID_NAMESPACE = '0e9b3c7a-5d1f-42a8-b6c4-e2d0f8a17593';
46
+ function createStableChunkId(code) {
47
+ const namespace = Buffer.from(CHUNK_ID_NAMESPACE.replace(/-/g, ''), 'hex');
48
+ const hash = external_crypto_namespaceObject.createHash('sha1').update(namespace).update(code, 'utf8').digest();
49
+ hash[6] = 0x0f & hash[6] | 0x50;
50
+ hash[8] = 0x3f & hash[8] | 0x80;
51
+ const hex = hash.subarray(0, 16).toString('hex');
52
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
53
+ }
54
+ const COMMENT_REGEX = /^\/\/# chunkId=(\S{1,128})/;
55
+ const COMMENT_MARKER = '//# chunkId=';
56
+ const WINDOW = 256;
57
+ function determineChunkIdFromSource(code) {
58
+ for(let at = code.indexOf(COMMENT_MARKER); -1 !== at; at = code.indexOf(COMMENT_MARKER, at + COMMENT_MARKER.length)){
59
+ if (0 !== at && '\n' !== code[at - 1]) continue;
60
+ const match = COMMENT_REGEX.exec(code.slice(at, at + WINDOW));
61
+ if (match) return match[1];
62
+ }
63
+ }
64
+ exports.createChunkId = __webpack_exports__.createChunkId;
65
+ exports.createChunkIdComment = __webpack_exports__.createChunkIdComment;
66
+ exports.createChunkIdSnippet = __webpack_exports__.createChunkIdSnippet;
67
+ exports.createStableChunkId = __webpack_exports__.createStableChunkId;
68
+ exports.determineChunkIdFromSource = __webpack_exports__.determineChunkIdFromSource;
69
+ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
70
+ "createChunkId",
71
+ "createChunkIdComment",
72
+ "createChunkIdSnippet",
73
+ "createStableChunkId",
74
+ "determineChunkIdFromSource"
75
+ ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
76
+ Object.defineProperty(exports, '__esModule', {
77
+ value: true
78
+ });
@@ -0,0 +1,32 @@
1
+ import { createHash, randomUUID } from "crypto";
2
+ function createChunkIdSnippet(chunkId, releaseId) {
3
+ const chunk = JSON.stringify(chunkId);
4
+ if (void 0 === releaseId) return `!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._posthogChunkIds=e._posthogChunkIds||{},e._posthogChunkIds[n]=${chunk})}catch(e){}}();`;
5
+ return `!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e._posthogReleaseId=e._posthogReleaseId||${JSON.stringify(releaseId)};var n=(new e.Error).stack;n&&(e._posthogChunkIds=e._posthogChunkIds||{},e._posthogChunkIds[n]=${chunk})}catch(e){}}();`;
6
+ }
7
+ function createChunkIdComment(chunkId) {
8
+ return `\n//# chunkId=${chunkId}`;
9
+ }
10
+ function createChunkId() {
11
+ return randomUUID();
12
+ }
13
+ const CHUNK_ID_NAMESPACE = '0e9b3c7a-5d1f-42a8-b6c4-e2d0f8a17593';
14
+ function createStableChunkId(code) {
15
+ const namespace = Buffer.from(CHUNK_ID_NAMESPACE.replace(/-/g, ''), 'hex');
16
+ const hash = createHash('sha1').update(namespace).update(code, 'utf8').digest();
17
+ hash[6] = 0x0f & hash[6] | 0x50;
18
+ hash[8] = 0x3f & hash[8] | 0x80;
19
+ const hex = hash.subarray(0, 16).toString('hex');
20
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
21
+ }
22
+ const COMMENT_REGEX = /^\/\/# chunkId=(\S{1,128})/;
23
+ const COMMENT_MARKER = '//# chunkId=';
24
+ const WINDOW = 256;
25
+ function determineChunkIdFromSource(code) {
26
+ for(let at = code.indexOf(COMMENT_MARKER); -1 !== at; at = code.indexOf(COMMENT_MARKER, at + COMMENT_MARKER.length)){
27
+ if (0 !== at && '\n' !== code[at - 1]) continue;
28
+ const match = COMMENT_REGEX.exec(code.slice(at, at + WINDOW));
29
+ if (match) return match[1];
30
+ }
31
+ }
32
+ export { createChunkId, createChunkIdComment, createChunkIdSnippet, createStableChunkId, determineChunkIdFromSource };
package/dist/cli.d.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  import { ResolvedPluginConfig } from './config';
2
2
  /**
3
- * Build CLI arguments for `posthog-cli sourcemap process`.
3
+ * `process` injects chunk ids into the built files on disk and uploads them.
4
+ * `upload` only reads the files, expecting chunk ids to already be present
5
+ * (e.g. injected in-memory by a bundler plugin before the files were written).
6
+ */
7
+ export type SourcemapCliCommand = 'process' | 'upload';
8
+ /**
9
+ * Build CLI arguments for `posthog-cli sourcemap <command>`.
4
10
  */
5
11
  export declare function buildSourcemapCliArgs(config: ResolvedPluginConfig, mode: {
6
12
  stdin: true;
7
13
  } | {
8
14
  directory: string;
9
- }): string[];
15
+ }, command?: SourcemapCliCommand): string[];
10
16
  /**
11
17
  * Build environment variables for CLI invocation.
12
18
  * Plugin config values override any existing process.env values.
@@ -15,9 +21,19 @@ export declare function buildCliEnv(config: ResolvedPluginConfig): NodeJS.Proces
15
21
  /**
16
22
  * Spawn the PostHog CLI for sourcemap processing via stdin (file list).
17
23
  */
18
- export declare function runSourcemapCli(config: ResolvedPluginConfig, options: {
24
+ export declare function runSourcemapCli(config: ResolvedPluginConfig, options: ({
19
25
  filePaths: string[];
20
26
  } | {
21
27
  directory: string;
28
+ }) & {
29
+ command?: SourcemapCliCommand;
22
30
  }): Promise<void>;
31
+ /**
32
+ * Resolves the release this build belongs to, creating it if it doesn't exist yet, and returns its
33
+ * id for injection into the chunks. Returns undefined when nothing identifies a release: the CLI
34
+ * prints nothing and exits zero for a build with no release name/version and no git or CI
35
+ * metadata to derive them from, which is a build that ships without a release rather than a
36
+ * failure.
37
+ */
38
+ export declare function resolveReleaseId(config: ResolvedPluginConfig): Promise<string | undefined>;
23
39
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAG/C;;GAEG;AACH,wBAAgB,qBAAqB,CACjC,MAAM,EAAE,oBAAoB,EAC5B,IAAI,EAAE;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAC9C,MAAM,EAAE,CA8BV;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAAC,UAAU,CAQ3E;AAED;;GAEG;AACH,wBAAsB,eAAe,CACjC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GACzD,OAAO,CAAC,IAAI,CAAC,CAgBf"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAG/C;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,QAAQ,CAAA;AAwBtD;;GAEG;AACH,wBAAgB,qBAAqB,CACjC,MAAM,EAAE,oBAAoB,EAC5B,IAAI,EAAE;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EAC7C,OAAO,GAAE,mBAA+B,GACzC,MAAM,EAAE,CA8BV;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAAC,UAAU,CAW3E;AAED;;GAEG;AACH,wBAAsB,eAAe,CACjC,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,CAAC;IAAE,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG;IAAE,OAAO,CAAC,EAAE,mBAAmB,CAAA;CAAE,GAC/F,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAuBhG"}
package/dist/cli.js CHANGED
@@ -24,22 +24,29 @@ var __webpack_require__ = {};
24
24
  var __webpack_exports__ = {};
25
25
  __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
- runSourcemapCli: ()=>runSourcemapCli,
27
+ resolveReleaseId: ()=>resolveReleaseId,
28
28
  buildCliEnv: ()=>buildCliEnv,
29
+ runSourcemapCli: ()=>runSourcemapCli,
29
30
  buildSourcemapCliArgs: ()=>buildSourcemapCliArgs
30
31
  });
31
32
  const external_spawn_local_js_namespaceObject = require("./spawn-local.js");
32
- function buildSourcemapCliArgs(config, mode) {
33
+ function buildReleaseArgs(config) {
34
+ const args = [];
35
+ if (config.sourcemaps.releaseName) args.push('--release-name', config.sourcemaps.releaseName);
36
+ if (config.sourcemaps.releaseVersion) args.push('--release-version', config.sourcemaps.releaseVersion);
37
+ if (void 0 !== config.sourcemaps.build && '' !== config.sourcemaps.build) args.push('--build', config.sourcemaps.build);
38
+ return args;
39
+ }
40
+ function buildSourcemapCliArgs(config, mode, command = 'process') {
33
41
  const args = [
34
42
  'sourcemap',
35
- 'process'
43
+ command
36
44
  ];
37
45
  if ('stdin' in mode) args.push('--stdin');
38
46
  else args.push('--directory', mode.directory);
39
- if (config.sourcemaps.releaseName) args.push('--release-name', config.sourcemaps.releaseName);
40
- if (config.sourcemaps.releaseVersion) args.push('--release-version', config.sourcemaps.releaseVersion);
41
- if (void 0 !== config.sourcemaps.build && '' !== config.sourcemaps.build) args.push('--build', config.sourcemaps.build);
42
- if (config.sourcemaps.deleteAfterUpload) args.push('--delete-after');
47
+ args.push(...buildReleaseArgs(config));
48
+ if ('event' === config.sourcemaps.releaseMode) args.push('--release-mode', 'event');
49
+ if (config.sourcemaps.deleteAfterUpload && 'process' === command) args.push('--delete-after');
43
50
  if (config.sourcemaps.batchSize) args.push('--batch-size', config.sourcemaps.batchSize.toString());
44
51
  return args;
45
52
  }
@@ -49,7 +56,8 @@ function buildCliEnv(config) {
49
56
  RUST_LOG: `posthog_cli=${config.logLevel}`,
50
57
  POSTHOG_CLI_HOST: config.host,
51
58
  POSTHOG_CLI_API_KEY: config.personalApiKey,
52
- POSTHOG_CLI_PROJECT_ID: config.projectId
59
+ POSTHOG_CLI_PROJECT_ID: config.projectId,
60
+ POSTHOG_RELEASE_MODE: config.sourcemaps.releaseMode
53
61
  };
54
62
  }
55
63
  async function runSourcemapCli(config, options) {
@@ -58,7 +66,7 @@ async function runSourcemapCli(config, options) {
58
66
  } : {
59
67
  directory: options.directory
60
68
  };
61
- const args = buildSourcemapCliArgs(config, mode);
69
+ const args = buildSourcemapCliArgs(config, mode, options.command ?? 'process');
62
70
  const env = buildCliEnv(config);
63
71
  const spawnOptions = {
64
72
  cwd: process.cwd(),
@@ -68,12 +76,31 @@ async function runSourcemapCli(config, options) {
68
76
  if ('filePaths' in options) spawnOptions.stdin = options.filePaths.join('\n') + '\n';
69
77
  await (0, external_spawn_local_js_namespaceObject.spawnLocal)(config.cliBinaryPath, args, spawnOptions);
70
78
  }
79
+ async function resolveReleaseId(config) {
80
+ const args = [
81
+ 'release',
82
+ 'resolve',
83
+ ...buildReleaseArgs(config)
84
+ ];
85
+ const { code, stdout, stderr } = await (0, external_spawn_local_js_namespaceObject.spawnLocalCapture)(config.cliBinaryPath, args, {
86
+ cwd: process.cwd(),
87
+ env: buildCliEnv(config)
88
+ });
89
+ if (0 !== code) {
90
+ const hint = /unrecognized subcommand|unexpected argument/.test(stderr) ? ` Event release mode needs a posthog-cli with 'release resolve' (${config.cliBinaryPath}).` : '';
91
+ throw new Error(`posthog-cli release resolve failed with code ${code}.${hint}\n${stderr.trim()}`);
92
+ }
93
+ if (stderr.trim()) process.stderr.write(stderr);
94
+ return stdout.trim() || void 0;
95
+ }
71
96
  exports.buildCliEnv = __webpack_exports__.buildCliEnv;
72
97
  exports.buildSourcemapCliArgs = __webpack_exports__.buildSourcemapCliArgs;
98
+ exports.resolveReleaseId = __webpack_exports__.resolveReleaseId;
73
99
  exports.runSourcemapCli = __webpack_exports__.runSourcemapCli;
74
100
  for(var __webpack_i__ in __webpack_exports__)if (-1 === [
75
101
  "buildCliEnv",
76
102
  "buildSourcemapCliArgs",
103
+ "resolveReleaseId",
77
104
  "runSourcemapCli"
78
105
  ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
79
106
  Object.defineProperty(exports, '__esModule', {
package/dist/cli.mjs CHANGED
@@ -1,15 +1,21 @@
1
- import { spawnLocal } from "./spawn-local.mjs";
2
- function buildSourcemapCliArgs(config, mode) {
1
+ import { spawnLocal, spawnLocalCapture } from "./spawn-local.mjs";
2
+ function buildReleaseArgs(config) {
3
+ const args = [];
4
+ if (config.sourcemaps.releaseName) args.push('--release-name', config.sourcemaps.releaseName);
5
+ if (config.sourcemaps.releaseVersion) args.push('--release-version', config.sourcemaps.releaseVersion);
6
+ if (void 0 !== config.sourcemaps.build && '' !== config.sourcemaps.build) args.push('--build', config.sourcemaps.build);
7
+ return args;
8
+ }
9
+ function buildSourcemapCliArgs(config, mode, command = 'process') {
3
10
  const args = [
4
11
  'sourcemap',
5
- 'process'
12
+ command
6
13
  ];
7
14
  if ('stdin' in mode) args.push('--stdin');
8
15
  else args.push('--directory', mode.directory);
9
- if (config.sourcemaps.releaseName) args.push('--release-name', config.sourcemaps.releaseName);
10
- if (config.sourcemaps.releaseVersion) args.push('--release-version', config.sourcemaps.releaseVersion);
11
- if (void 0 !== config.sourcemaps.build && '' !== config.sourcemaps.build) args.push('--build', config.sourcemaps.build);
12
- if (config.sourcemaps.deleteAfterUpload) args.push('--delete-after');
16
+ args.push(...buildReleaseArgs(config));
17
+ if ('event' === config.sourcemaps.releaseMode) args.push('--release-mode', 'event');
18
+ if (config.sourcemaps.deleteAfterUpload && 'process' === command) args.push('--delete-after');
13
19
  if (config.sourcemaps.batchSize) args.push('--batch-size', config.sourcemaps.batchSize.toString());
14
20
  return args;
15
21
  }
@@ -19,7 +25,8 @@ function buildCliEnv(config) {
19
25
  RUST_LOG: `posthog_cli=${config.logLevel}`,
20
26
  POSTHOG_CLI_HOST: config.host,
21
27
  POSTHOG_CLI_API_KEY: config.personalApiKey,
22
- POSTHOG_CLI_PROJECT_ID: config.projectId
28
+ POSTHOG_CLI_PROJECT_ID: config.projectId,
29
+ POSTHOG_RELEASE_MODE: config.sourcemaps.releaseMode
23
30
  };
24
31
  }
25
32
  async function runSourcemapCli(config, options) {
@@ -28,7 +35,7 @@ async function runSourcemapCli(config, options) {
28
35
  } : {
29
36
  directory: options.directory
30
37
  };
31
- const args = buildSourcemapCliArgs(config, mode);
38
+ const args = buildSourcemapCliArgs(config, mode, options.command ?? 'process');
32
39
  const env = buildCliEnv(config);
33
40
  const spawnOptions = {
34
41
  cwd: process.cwd(),
@@ -38,4 +45,21 @@ async function runSourcemapCli(config, options) {
38
45
  if ('filePaths' in options) spawnOptions.stdin = options.filePaths.join('\n') + '\n';
39
46
  await spawnLocal(config.cliBinaryPath, args, spawnOptions);
40
47
  }
41
- export { buildCliEnv, buildSourcemapCliArgs, runSourcemapCli };
48
+ async function resolveReleaseId(config) {
49
+ const args = [
50
+ 'release',
51
+ 'resolve',
52
+ ...buildReleaseArgs(config)
53
+ ];
54
+ const { code, stdout, stderr } = await spawnLocalCapture(config.cliBinaryPath, args, {
55
+ cwd: process.cwd(),
56
+ env: buildCliEnv(config)
57
+ });
58
+ if (0 !== code) {
59
+ const hint = /unrecognized subcommand|unexpected argument/.test(stderr) ? ` Event release mode needs a posthog-cli with 'release resolve' (${config.cliBinaryPath}).` : '';
60
+ throw new Error(`posthog-cli release resolve failed with code ${code}.${hint}\n${stderr.trim()}`);
61
+ }
62
+ if (stderr.trim()) process.stderr.write(stderr);
63
+ return stdout.trim() || void 0;
64
+ }
65
+ export { buildCliEnv, buildSourcemapCliArgs, resolveReleaseId, runSourcemapCli };
package/dist/config.d.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
2
+ /**
3
+ * How an exception gets associated with a release, mirroring posthog-cli's `--release-mode`.
4
+ *
5
+ * `symbol-set` binds the uploaded symbol sets to a release. `event` leaves them unbound and
6
+ * injects the release id into each chunk instead, so the release is resolved per exception.
7
+ */
8
+ export type ReleaseMode = 'symbol-set' | 'event';
2
9
  export interface PluginConfig {
3
10
  personalApiKey: string;
4
11
  /** @deprecated Use projectId instead */
@@ -18,6 +25,12 @@ export interface PluginConfig {
18
25
  build?: string | number;
19
26
  deleteAfterUpload?: boolean;
20
27
  batchSize?: number;
28
+ /**
29
+ * EXPERIMENTAL. Defaults to the `POSTHOG_RELEASE_MODE` env var, the same one posthog-cli
30
+ * reads, then to `symbol-set`. Event mode needs a posthog-cli that supports
31
+ * `release resolve` and `--release-mode`.
32
+ */
33
+ releaseMode?: ReleaseMode;
21
34
  };
22
35
  }
23
36
  export interface ResolvedPluginConfig extends Omit<PluginConfig, 'envId' | 'projectId'> {
@@ -32,6 +45,7 @@ export interface ResolvedPluginConfig extends Omit<PluginConfig, 'envId' | 'proj
32
45
  build?: string;
33
46
  deleteAfterUpload: boolean;
34
47
  batchSize?: number;
48
+ releaseMode: ReleaseMode;
35
49
  };
36
50
  }
37
51
  export interface ResolveConfigOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;AAErE,MAAM,WAAW,YAAY;IACzB,cAAc,EAAE,MAAM,CAAA;IACtB,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,UAAU,CAAC,EAAE;QACT,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,0CAA0C;QAC1C,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,6CAA6C;QAC7C,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;QACvB,iBAAiB,CAAC,EAAE,OAAO,CAAA;QAC3B,SAAS,CAAC,EAAE,MAAM,CAAA;KACrB,CAAA;CACJ;AAED,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,WAAW,CAAC;IACnF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,QAAQ,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE;QACR,OAAO,EAAE,OAAO,CAAA;QAChB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,iBAAiB,EAAE,OAAO,CAAA;QAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;KACrB,CAAA;CACJ;AAED,MAAM,WAAW,oBAAoB;IACjC,sFAAsF;IACtF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,iFAAiF;IACjF,GAAG,CAAC,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,cAAc,CAAC,EAAE,oBAAoB,GAAG,oBAAoB,CAyChH"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;AAErE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,OAAO,CAAA;AAchD,MAAM,WAAW,YAAY;IACzB,cAAc,EAAE,MAAM,CAAA;IACtB,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,UAAU,CAAC,EAAE;QACT,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,0CAA0C;QAC1C,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,6CAA6C;QAC7C,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;QACvB,iBAAiB,CAAC,EAAE,OAAO,CAAA;QAC3B,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB;;;;WAIG;QACH,WAAW,CAAC,EAAE,WAAW,CAAA;KAC5B,CAAA;CACJ;AAED,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,WAAW,CAAC;IACnF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,QAAQ,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE;QACR,OAAO,EAAE,OAAO,CAAA;QAChB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,cAAc,CAAC,EAAE,MAAM,CAAA;QACvB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,iBAAiB,EAAE,OAAO,CAAA;QAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,WAAW,EAAE,WAAW,CAAA;KAC3B,CAAA;CACJ;AAED,MAAM,WAAW,oBAAoB;IACjC,sFAAsF;IACtF,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,iFAAiF;IACjF,GAAG,CAAC,EAAE,MAAM,CAAA;CACf;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,cAAc,CAAC,EAAE,oBAAoB,GAAG,oBAAoB,CA0ChH"}
package/dist/config.js CHANGED
@@ -35,6 +35,15 @@ function normalizeHost(value) {
35
35
  const normalizedHost = 'string' == typeof value ? value.trim() : '';
36
36
  return normalizedHost || DEFAULT_PLUGIN_HOST;
37
37
  }
38
+ const RELEASE_MODES = [
39
+ 'symbol-set',
40
+ 'event'
41
+ ];
42
+ function normalizeReleaseMode(value) {
43
+ if (void 0 === value || '' === value) return 'symbol-set';
44
+ if (!RELEASE_MODES.includes(value)) throw new Error(`sourcemaps.releaseMode must be one of ${RELEASE_MODES.join(', ')}, got '${value}'`);
45
+ return value;
46
+ }
38
47
  function resolveConfig(options, resolveOptions) {
39
48
  const projectId = options.projectId ?? options.envId;
40
49
  const personalApiKey = normalizeApiKey(options.personalApiKey);
@@ -64,7 +73,8 @@ function resolveConfig(options, resolveOptions) {
64
73
  releaseVersion: userSourcemaps.releaseVersion ?? userSourcemaps.version,
65
74
  build: void 0 !== userSourcemaps.build ? String(userSourcemaps.build) : void 0,
66
75
  deleteAfterUpload: userSourcemaps.deleteAfterUpload ?? true,
67
- batchSize: userSourcemaps.batchSize
76
+ batchSize: userSourcemaps.batchSize,
77
+ releaseMode: normalizeReleaseMode(userSourcemaps.releaseMode ?? process.env.POSTHOG_RELEASE_MODE)
68
78
  }
69
79
  };
70
80
  }
package/dist/config.mjs CHANGED
@@ -7,6 +7,15 @@ function normalizeHost(value) {
7
7
  const normalizedHost = 'string' == typeof value ? value.trim() : '';
8
8
  return normalizedHost || DEFAULT_PLUGIN_HOST;
9
9
  }
10
+ const RELEASE_MODES = [
11
+ 'symbol-set',
12
+ 'event'
13
+ ];
14
+ function normalizeReleaseMode(value) {
15
+ if (void 0 === value || '' === value) return 'symbol-set';
16
+ if (!RELEASE_MODES.includes(value)) throw new Error(`sourcemaps.releaseMode must be one of ${RELEASE_MODES.join(', ')}, got '${value}'`);
17
+ return value;
18
+ }
10
19
  function resolveConfig(options, resolveOptions) {
11
20
  const projectId = options.projectId ?? options.envId;
12
21
  const personalApiKey = normalizeApiKey(options.personalApiKey);
@@ -36,7 +45,8 @@ function resolveConfig(options, resolveOptions) {
36
45
  releaseVersion: userSourcemaps.releaseVersion ?? userSourcemaps.version,
37
46
  build: void 0 !== userSourcemaps.build ? String(userSourcemaps.build) : void 0,
38
47
  deleteAfterUpload: userSourcemaps.deleteAfterUpload ?? true,
39
- batchSize: userSourcemaps.batchSize
48
+ batchSize: userSourcemaps.batchSize,
49
+ releaseMode: normalizeReleaseMode(userSourcemaps.releaseMode ?? process.env.POSTHOG_RELEASE_MODE)
40
50
  }
41
51
  };
42
52
  }
package/dist/index.d.ts CHANGED
@@ -2,4 +2,5 @@ export * from './spawn-local';
2
2
  export { resolveBinaryPath, buildLocalBinaryPaths } from './utils';
3
3
  export * from './config';
4
4
  export * from './cli';
5
+ export * from './chunk-ids';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AAClE,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAA;AAC7B,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AAClE,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,aAAa,CAAA"}
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  "use strict";
2
2
  var __webpack_modules__ = {
3
+ "./chunk-ids": function(module) {
4
+ module.exports = require("./chunk-ids.js");
5
+ },
3
6
  "./cli": function(module) {
4
7
  module.exports = require("./cli.js");
5
8
  },
@@ -91,6 +94,16 @@ var __webpack_exports__ = {};
91
94
  return _cli__WEBPACK_IMPORTED_MODULE_3__[key];
92
95
  }).bind(0, __WEBPACK_IMPORT_KEY__);
93
96
  __webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
97
+ var _chunk_ids__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__("./chunk-ids");
98
+ var __WEBPACK_REEXPORT_OBJECT__ = {};
99
+ for(var __WEBPACK_IMPORT_KEY__ in _chunk_ids__WEBPACK_IMPORTED_MODULE_4__)if ([
100
+ "buildLocalBinaryPaths",
101
+ "resolveBinaryPath",
102
+ "default"
103
+ ].indexOf(__WEBPACK_IMPORT_KEY__) < 0) __WEBPACK_REEXPORT_OBJECT__[__WEBPACK_IMPORT_KEY__] = (function(key) {
104
+ return _chunk_ids__WEBPACK_IMPORTED_MODULE_4__[key];
105
+ }).bind(0, __WEBPACK_IMPORT_KEY__);
106
+ __webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__);
94
107
  })();
95
108
  exports.buildLocalBinaryPaths = __webpack_exports__.buildLocalBinaryPaths;
96
109
  exports.resolveBinaryPath = __webpack_exports__.resolveBinaryPath;
package/dist/index.mjs CHANGED
@@ -2,4 +2,5 @@ import { buildLocalBinaryPaths, resolveBinaryPath } from "./utils.mjs";
2
2
  export * from "./spawn-local.mjs";
3
3
  export * from "./config.mjs";
4
4
  export * from "./cli.mjs";
5
+ export * from "./chunk-ids.mjs";
5
6
  export { buildLocalBinaryPaths, resolveBinaryPath };
@@ -1,3 +1,17 @@
1
+ export interface CapturedRun {
2
+ code: number | null;
3
+ stdout: string;
4
+ stderr: string;
5
+ }
6
+ /**
7
+ * Runs a command and captures both streams instead of inheriting them, for commands whose stdout
8
+ * is a value the caller needs rather than build output. Resolves with a non-zero `code` rather
9
+ * than rejecting, so the caller can shape the error from what the command wrote to stderr.
10
+ */
11
+ export declare function spawnLocalCapture(executable: string, args: string[], options: {
12
+ env: NodeJS.ProcessEnv;
13
+ cwd: string;
14
+ }): Promise<CapturedRun>;
1
15
  export declare function spawnLocal(executable: string, args: string[], options: {
2
16
  env: NodeJS.ProcessEnv;
3
17
  stdio: 'inherit' | 'ignore';
@@ -1 +1 @@
1
- {"version":3,"file":"spawn-local.d.ts","sourceRoot":"","sources":["../src/spawn-local.ts"],"names":[],"mappings":"AAEA,wBAAsB,UAAU,CAC5B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IACL,GAAG,EAAE,MAAM,CAAC,UAAU,CAAA;IACtB,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAA;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB,GACF,OAAO,CAAC,IAAI,CAAC,CAiCf"}
1
+ {"version":3,"file":"spawn-local.d.ts","sourceRoot":"","sources":["../src/spawn-local.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACjB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACnC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IACL,GAAG,EAAE,MAAM,CAAC,UAAU,CAAA;IACtB,GAAG,EAAE,MAAM,CAAA;CACd,GACF,OAAO,CAAC,WAAW,CAAC,CAoBtB;AAED,wBAAsB,UAAU,CAC5B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IACL,GAAG,EAAE,MAAM,CAAC,UAAU,CAAA;IACtB,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAA;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB,GACF,OAAO,CAAC,IAAI,CAAC,CAiCf"}
@@ -24,9 +24,39 @@ var __webpack_require__ = {};
24
24
  var __webpack_exports__ = {};
25
25
  __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
+ spawnLocalCapture: ()=>spawnLocalCapture,
27
28
  spawnLocal: ()=>spawnLocal
28
29
  });
29
30
  const external_cross_spawn_namespaceObject = require("cross-spawn");
31
+ async function spawnLocalCapture(executable, args, options) {
32
+ const child = (0, external_cross_spawn_namespaceObject.spawn)(executable, [
33
+ ...args
34
+ ], {
35
+ stdio: [
36
+ 'ignore',
37
+ 'pipe',
38
+ 'pipe'
39
+ ],
40
+ env: options.env,
41
+ cwd: options.cwd
42
+ });
43
+ let stdout = '';
44
+ let stderr = '';
45
+ child.stdout?.on('data', (chunk)=>{
46
+ stdout += chunk.toString();
47
+ });
48
+ child.stderr?.on('data', (chunk)=>{
49
+ stderr += chunk.toString();
50
+ });
51
+ return await new Promise((resolve, reject)=>{
52
+ child.on('close', (code)=>resolve({
53
+ code,
54
+ stdout,
55
+ stderr
56
+ }));
57
+ child.on('error', (error)=>reject(error));
58
+ });
59
+ }
30
60
  async function spawnLocal(executable, args, options) {
31
61
  const stdioOption = void 0 !== options.stdin ? [
32
62
  'pipe',
@@ -58,8 +88,10 @@ async function spawnLocal(executable, args, options) {
58
88
  });
59
89
  }
60
90
  exports.spawnLocal = __webpack_exports__.spawnLocal;
91
+ exports.spawnLocalCapture = __webpack_exports__.spawnLocalCapture;
61
92
  for(var __webpack_i__ in __webpack_exports__)if (-1 === [
62
- "spawnLocal"
93
+ "spawnLocal",
94
+ "spawnLocalCapture"
63
95
  ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
64
96
  Object.defineProperty(exports, '__esModule', {
65
97
  value: true
@@ -1,4 +1,33 @@
1
1
  import { spawn } from "cross-spawn";
2
+ async function spawnLocalCapture(executable, args, options) {
3
+ const child = spawn(executable, [
4
+ ...args
5
+ ], {
6
+ stdio: [
7
+ 'ignore',
8
+ 'pipe',
9
+ 'pipe'
10
+ ],
11
+ env: options.env,
12
+ cwd: options.cwd
13
+ });
14
+ let stdout = '';
15
+ let stderr = '';
16
+ child.stdout?.on('data', (chunk)=>{
17
+ stdout += chunk.toString();
18
+ });
19
+ child.stderr?.on('data', (chunk)=>{
20
+ stderr += chunk.toString();
21
+ });
22
+ return await new Promise((resolve, reject)=>{
23
+ child.on('close', (code)=>resolve({
24
+ code,
25
+ stdout,
26
+ stderr
27
+ }));
28
+ child.on('error', (error)=>reject(error));
29
+ });
30
+ }
2
31
  async function spawnLocal(executable, args, options) {
3
32
  const stdioOption = void 0 !== options.stdin ? [
4
33
  'pipe',
@@ -29,4 +58,4 @@ async function spawnLocal(executable, args, options) {
29
58
  });
30
59
  });
31
60
  }
32
- export { spawnLocal };
61
+ export { spawnLocal, spawnLocalCapture };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/plugin-utils",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "bugs": {
5
5
  "url": "https://github.com/PostHog/posthog-js/issues"
6
6
  },
@@ -0,0 +1,96 @@
1
+ import * as crypto from 'crypto'
2
+
3
+ /**
4
+ * Returns the minified chunk-id IIFE the SDK reads at runtime to map an
5
+ * error's stack back to a chunk id. With `releaseId`, the snippet also pins
6
+ * `_posthogReleaseId` on the global so the SDK emits the release on every
7
+ * exception (first write wins, so the first loaded chunk decides).
8
+ *
9
+ * Byte-for-byte contract with posthog-cli (`CODE_SNIPPET_TEMPLATE` and
10
+ * `CODE_SNIPPET_WITH_RELEASE_TEMPLATE` in cli/src/sourcemaps/constant.rs): the
11
+ * CLI recognizes and removes previously injected snippets by exact substring
12
+ * match, so any change here must ship in the CLI constants too. Ids are
13
+ * JSON-encoded for the same reason the CLI encodes them: an id carrying a quote
14
+ * would otherwise break out of the string literal. Covered by a parity test in
15
+ * chunk-ids.spec.ts.
16
+ */
17
+ export function createChunkIdSnippet(chunkId: string, releaseId?: string): string {
18
+ const chunk = JSON.stringify(chunkId)
19
+ if (releaseId === undefined) {
20
+ return `!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._posthogChunkIds=e._posthogChunkIds||{},e._posthogChunkIds[n]=${chunk})}catch(e){}}();`
21
+ }
22
+ return `!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e._posthogReleaseId=e._posthogReleaseId||${JSON.stringify(releaseId)};var n=(new e.Error).stack;n&&(e._posthogChunkIds=e._posthogChunkIds||{},e._posthogChunkIds[n]=${chunk})}catch(e){}}();`
23
+ }
24
+
25
+ /**
26
+ * Returns the comment posthog-cli discovers chunk ids from at upload time
27
+ * (`CHUNKID_COMMENT_PREFIX` in cli/src/sourcemaps/constant.rs), including the
28
+ * leading newline the CLI's exact-match removal expects.
29
+ */
30
+ export function createChunkIdComment(chunkId: string): string {
31
+ return `\n//# chunkId=${chunkId}`
32
+ }
33
+
34
+ /**
35
+ * Mints a fresh random chunk id: a new id per chunk, per build.
36
+ */
37
+ export function createChunkId(): string {
38
+ return crypto.randomUUID()
39
+ }
40
+
41
+ // Fixed namespace for content-addressed chunk ids, matching `CHUNK_ID_NAMESPACE` in
42
+ // cli/src/sourcemaps/constant.rs. Both sides must derive ids from the same namespace, or a
43
+ // dist processed by one tool and re-processed by the other would upload the same code twice.
44
+ const CHUNK_ID_NAMESPACE = '0e9b3c7a-5d1f-42a8-b6c4-e2d0f8a17593'
45
+
46
+ /**
47
+ * Derives a chunk id from the chunk's own bytes (UUIDv5), so identical code keeps its id across
48
+ * rebuilds and re-uploads dedupe against the symbol set already stored. Used in event release
49
+ * mode, where the release travels inside the chunk instead of being bound to the symbol set, so
50
+ * a per-build random id would create a fresh symbol set on every build.
51
+ *
52
+ * The id is content-addressed over what `renderChunk` sees. posthog-cli derives its own ids from
53
+ * the file on disk, which by then also carries the trailing `sourceMappingURL` comment, so the
54
+ * same chunk gets a different (but equally stable) id depending on which tool injected it.
55
+ */
56
+ export function createStableChunkId(code: string): string {
57
+ const namespace = Buffer.from(CHUNK_ID_NAMESPACE.replace(/-/g, ''), 'hex')
58
+ const hash = crypto.createHash('sha1').update(namespace).update(code, 'utf8').digest()
59
+
60
+ // RFC 4122: version in the high nibble of byte 6, variant in the top two bits of byte 8.
61
+ hash[6] = (hash[6] & 0x0f) | 0x50
62
+ hash[8] = (hash[8] & 0x3f) | 0x80
63
+
64
+ const hex = hash.subarray(0, 16).toString('hex')
65
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`
66
+ }
67
+
68
+ // Anchored with a bounded quantifier and only ever run against a fixed-size
69
+ // window: an unbounded regex over the whole bundle is quadratic on adversarial
70
+ // input (a match attempt per marker occurrence, each scanning ahead).
71
+ const COMMENT_REGEX = /^\/\/# chunkId=(\S{1,128})/
72
+ const COMMENT_MARKER = '//# chunkId='
73
+ const WINDOW = 256
74
+
75
+ /**
76
+ * Extracts the chunk id from source that already carries one, using the same
77
+ * detection the CLI uses: a `//# chunkId=` comment at the start of a line.
78
+ * The injected IIFE is deliberately not matched — its marker text can appear
79
+ * inside string literals of bundled code (docs strings, bundled PostHog
80
+ * tooling), and the CLI only ever reads the line-anchored comment. Keeping the
81
+ * two in sync guarantees this returning an id ⟺ the CLI finding one.
82
+ */
83
+ export function determineChunkIdFromSource(code: string): string | undefined {
84
+ for (
85
+ let at = code.indexOf(COMMENT_MARKER);
86
+ at !== -1;
87
+ at = code.indexOf(COMMENT_MARKER, at + COMMENT_MARKER.length)
88
+ ) {
89
+ if (at !== 0 && code[at - 1] !== '\n') continue
90
+ const match = COMMENT_REGEX.exec(code.slice(at, at + WINDOW))
91
+ if (match) {
92
+ return match[1]
93
+ }
94
+ }
95
+ return undefined
96
+ }
package/src/cli.ts CHANGED
@@ -1,20 +1,19 @@
1
1
  import { ResolvedPluginConfig } from './config'
2
- import { spawnLocal } from './spawn-local'
2
+ import { spawnLocal, spawnLocalCapture } from './spawn-local'
3
3
 
4
4
  /**
5
- * Build CLI arguments for `posthog-cli sourcemap process`.
5
+ * `process` injects chunk ids into the built files on disk and uploads them.
6
+ * `upload` only reads the files, expecting chunk ids to already be present
7
+ * (e.g. injected in-memory by a bundler plugin before the files were written).
6
8
  */
7
- export function buildSourcemapCliArgs(
8
- config: ResolvedPluginConfig,
9
- mode: { stdin: true } | { directory: string }
10
- ): string[] {
11
- const args = ['sourcemap', 'process']
9
+ export type SourcemapCliCommand = 'process' | 'upload'
12
10
 
13
- if ('stdin' in mode) {
14
- args.push('--stdin')
15
- } else {
16
- args.push('--directory', mode.directory)
17
- }
11
+ /**
12
+ * The flags identifying which release this build belongs to. Shared by every command that
13
+ * resolves one, so `release resolve` lands on the same release row the upload would have.
14
+ */
15
+ function buildReleaseArgs(config: ResolvedPluginConfig): string[] {
16
+ const args: string[] = []
18
17
 
19
18
  if (config.sourcemaps.releaseName) {
20
19
  args.push('--release-name', config.sourcemaps.releaseName)
@@ -28,7 +27,38 @@ export function buildSourcemapCliArgs(
28
27
  args.push('--build', config.sourcemaps.build)
29
28
  }
30
29
 
31
- if (config.sourcemaps.deleteAfterUpload) {
30
+ return args
31
+ }
32
+
33
+ /**
34
+ * Build CLI arguments for `posthog-cli sourcemap <command>`.
35
+ */
36
+ export function buildSourcemapCliArgs(
37
+ config: ResolvedPluginConfig,
38
+ mode: { stdin: true } | { directory: string },
39
+ command: SourcemapCliCommand = 'process'
40
+ ): string[] {
41
+ const args = ['sourcemap', command]
42
+
43
+ if ('stdin' in mode) {
44
+ args.push('--stdin')
45
+ } else {
46
+ args.push('--directory', mode.directory)
47
+ }
48
+
49
+ args.push(...buildReleaseArgs(config))
50
+
51
+ // Only passed in event mode, so a symbol-set build keeps working against a posthog-cli
52
+ // predating the flag.
53
+ if (config.sourcemaps.releaseMode === 'event') {
54
+ args.push('--release-mode', 'event')
55
+ }
56
+
57
+ // On `upload` the caller owns map deletion: `--delete-after` also rewrites
58
+ // the .js files (stripping sourcemap references), and callers pick `upload`
59
+ // precisely because the written files must not change — e.g. Subresource
60
+ // Integrity hashes were already computed from them.
61
+ if (config.sourcemaps.deleteAfterUpload && command === 'process') {
32
62
  args.push('--delete-after')
33
63
  }
34
64
 
@@ -50,6 +80,9 @@ export function buildCliEnv(config: ResolvedPluginConfig): NodeJS.ProcessEnv {
50
80
  POSTHOG_CLI_HOST: config.host,
51
81
  POSTHOG_CLI_API_KEY: config.personalApiKey,
52
82
  POSTHOG_CLI_PROJECT_ID: config.projectId,
83
+ // The CLI reads this variable itself, so an inherited value would decide the release mode
84
+ // of the spawned command while the plugin injected chunks for the mode it resolved.
85
+ POSTHOG_RELEASE_MODE: config.sourcemaps.releaseMode,
53
86
  }
54
87
  }
55
88
 
@@ -58,10 +91,10 @@ export function buildCliEnv(config: ResolvedPluginConfig): NodeJS.ProcessEnv {
58
91
  */
59
92
  export async function runSourcemapCli(
60
93
  config: ResolvedPluginConfig,
61
- options: { filePaths: string[] } | { directory: string }
94
+ options: ({ filePaths: string[] } | { directory: string }) & { command?: SourcemapCliCommand }
62
95
  ): Promise<void> {
63
96
  const mode = 'filePaths' in options ? { stdin: true as const } : { directory: options.directory }
64
- const args = buildSourcemapCliArgs(config, mode)
97
+ const args = buildSourcemapCliArgs(config, mode, options.command ?? 'process')
65
98
  const env = buildCliEnv(config)
66
99
 
67
100
  const spawnOptions: Parameters<typeof spawnLocal>[2] = {
@@ -76,3 +109,35 @@ export async function runSourcemapCli(
76
109
 
77
110
  await spawnLocal(config.cliBinaryPath, args, spawnOptions)
78
111
  }
112
+
113
+ /**
114
+ * Resolves the release this build belongs to, creating it if it doesn't exist yet, and returns its
115
+ * id for injection into the chunks. Returns undefined when nothing identifies a release: the CLI
116
+ * prints nothing and exits zero for a build with no release name/version and no git or CI
117
+ * metadata to derive them from, which is a build that ships without a release rather than a
118
+ * failure.
119
+ */
120
+ export async function resolveReleaseId(config: ResolvedPluginConfig): Promise<string | undefined> {
121
+ const args = ['release', 'resolve', ...buildReleaseArgs(config)]
122
+ const { code, stdout, stderr } = await spawnLocalCapture(config.cliBinaryPath, args, {
123
+ cwd: process.cwd(),
124
+ env: buildCliEnv(config),
125
+ })
126
+
127
+ if (code !== 0) {
128
+ // `release resolve` shipped after `sourcemap upload`, so an older binary on the PATH
129
+ // fails here rather than at upload time, where the message would make more sense.
130
+ const hint = /unrecognized subcommand|unexpected argument/.test(stderr)
131
+ ? ` Event release mode needs a posthog-cli with 'release resolve' (${config.cliBinaryPath}).`
132
+ : ''
133
+ throw new Error(`posthog-cli release resolve failed with code ${code}.${hint}\n${stderr.trim()}`)
134
+ }
135
+
136
+ // The CLI logs to stderr and prints only the id to stdout, so forward its warnings rather
137
+ // than swallowing them with the captured stream.
138
+ if (stderr.trim()) {
139
+ process.stderr.write(stderr)
140
+ }
141
+
142
+ return stdout.trim() || undefined
143
+ }
package/src/config.ts CHANGED
@@ -13,6 +13,26 @@ function normalizeHost(value?: unknown): string {
13
13
 
14
14
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'
15
15
 
16
+ /**
17
+ * How an exception gets associated with a release, mirroring posthog-cli's `--release-mode`.
18
+ *
19
+ * `symbol-set` binds the uploaded symbol sets to a release. `event` leaves them unbound and
20
+ * injects the release id into each chunk instead, so the release is resolved per exception.
21
+ */
22
+ export type ReleaseMode = 'symbol-set' | 'event'
23
+
24
+ const RELEASE_MODES: ReleaseMode[] = ['symbol-set', 'event']
25
+
26
+ function normalizeReleaseMode(value: string | undefined): ReleaseMode {
27
+ if (value === undefined || value === '') {
28
+ return 'symbol-set'
29
+ }
30
+ if (!(RELEASE_MODES as string[]).includes(value)) {
31
+ throw new Error(`sourcemaps.releaseMode must be one of ${RELEASE_MODES.join(', ')}, got '${value}'`)
32
+ }
33
+ return value as ReleaseMode
34
+ }
35
+
16
36
  export interface PluginConfig {
17
37
  personalApiKey: string
18
38
  /** @deprecated Use projectId instead */
@@ -32,6 +52,12 @@ export interface PluginConfig {
32
52
  build?: string | number
33
53
  deleteAfterUpload?: boolean
34
54
  batchSize?: number
55
+ /**
56
+ * EXPERIMENTAL. Defaults to the `POSTHOG_RELEASE_MODE` env var, the same one posthog-cli
57
+ * reads, then to `symbol-set`. Event mode needs a posthog-cli that supports
58
+ * `release resolve` and `--release-mode`.
59
+ */
60
+ releaseMode?: ReleaseMode
35
61
  }
36
62
  }
37
63
 
@@ -47,6 +73,7 @@ export interface ResolvedPluginConfig extends Omit<PluginConfig, 'envId' | 'proj
47
73
  build?: string
48
74
  deleteAfterUpload: boolean
49
75
  batchSize?: number
76
+ releaseMode: ReleaseMode
50
77
  }
51
78
  }
52
79
 
@@ -96,6 +123,7 @@ export function resolveConfig(options: PluginConfig, resolveOptions?: ResolveCon
96
123
  build: userSourcemaps.build !== undefined ? String(userSourcemaps.build) : undefined,
97
124
  deleteAfterUpload: userSourcemaps.deleteAfterUpload ?? true,
98
125
  batchSize: userSourcemaps.batchSize,
126
+ releaseMode: normalizeReleaseMode(userSourcemaps.releaseMode ?? process.env.POSTHOG_RELEASE_MODE),
99
127
  },
100
128
  }
101
129
  }
package/src/index.ts CHANGED
@@ -2,3 +2,4 @@ export * from './spawn-local'
2
2
  export { resolveBinaryPath, buildLocalBinaryPaths } from './utils'
3
3
  export * from './config'
4
4
  export * from './cli'
5
+ export * from './chunk-ids'
@@ -1,5 +1,45 @@
1
1
  import { spawn } from 'cross-spawn'
2
2
 
3
+ export interface CapturedRun {
4
+ code: number | null
5
+ stdout: string
6
+ stderr: string
7
+ }
8
+
9
+ /**
10
+ * Runs a command and captures both streams instead of inheriting them, for commands whose stdout
11
+ * is a value the caller needs rather than build output. Resolves with a non-zero `code` rather
12
+ * than rejecting, so the caller can shape the error from what the command wrote to stderr.
13
+ */
14
+ export async function spawnLocalCapture(
15
+ executable: string,
16
+ args: string[],
17
+ options: {
18
+ env: NodeJS.ProcessEnv
19
+ cwd: string
20
+ }
21
+ ): Promise<CapturedRun> {
22
+ const child = spawn(executable, [...args], {
23
+ stdio: ['ignore', 'pipe', 'pipe'],
24
+ env: options.env,
25
+ cwd: options.cwd,
26
+ })
27
+
28
+ let stdout = ''
29
+ let stderr = ''
30
+ child.stdout?.on('data', (chunk: Buffer) => {
31
+ stdout += chunk.toString()
32
+ })
33
+ child.stderr?.on('data', (chunk: Buffer) => {
34
+ stderr += chunk.toString()
35
+ })
36
+
37
+ return await new Promise<CapturedRun>((resolve, reject) => {
38
+ child.on('close', (code) => resolve({ code, stdout, stderr }))
39
+ child.on('error', (error) => reject(error))
40
+ })
41
+ }
42
+
3
43
  export async function spawnLocal(
4
44
  executable: string,
5
45
  args: string[],