neon 8.8.0 → 8.9.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/dist/analytics.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { a as OAUTH, t as storeFor } from "./_chunks/credential_io-ByNuojoE.js";
2
2
  import { getCliAgent, getGithubEnvVars, isCi } from "./env.js";
3
3
  import { log } from "./log.js";
4
+ import { getOrCreateAnonymousId } from "./anonymous_id.js";
4
5
  import pkg_default from "./pkg.js";
5
6
  import { UnreadableResponseError, getApiClient, isNeonApiError } from "./api.js";
6
7
  import { getAuthContext } from "./auth_context.js";
@@ -19,16 +20,17 @@ const WRITE_KEY = "3SQXn5ejjXWLEJ8xU2PRYhAotLtTaeeV";
19
20
  const hasCurrentBranchArgv = () => process.argv.includes("--current-branch");
20
21
  const ANONYMOUS = "anonymous";
21
22
  /**
22
- * Who to attribute an event to, given whatever identified this invocation.
23
- *
24
- * Nothing is guaranteed to have identified it: a command can run with no credentials at all,
25
- * leaving the id empty. Segment accepts an empty `userId` and forwards it as-is rather than
26
- * rejecting it, so the substitution has to happen here. `""` is falsy but not nullish, which
27
- * is why the fallback has to be `||`.
23
+ * The identity to send an event under. A run nothing identified (an empty `userId`, which
24
+ * Segment would forward as-is) is the shared `"anonymous"` user, as it always was, so existing
25
+ * queries on it keep working. The stored anonymous id goes with every event, signed in or not,
26
+ * so the signed-out events of an installation can be joined to its account once a user is known.
28
27
  *
29
28
  * Exported for tests.
30
29
  */
31
- const analyticsUserId = (userId) => userId || ANONYMOUS;
30
+ const analyticsIdentity = (userId, anonymousId) => ({
31
+ userId: userId || ANONYMOUS,
32
+ ...anonymousId ? { anonymousId } : {}
33
+ });
32
34
  /**
33
35
  * The account an invocation that presented no API key may claim, which is nothing at all
34
36
  * unless stored credentials named a user.
@@ -77,10 +79,13 @@ const telemetryCredential = (authContext, apiKey, defaultCredentialsPath) => {
77
79
  let client;
78
80
  let clientInitialized = false;
79
81
  let errorEventContext;
82
+ /** Set with the client, so it exists only when analytics is on. */
83
+ let anonymousId;
80
84
  /** Lets tests send to a local collector; the CLI itself always uses track.neon.tech. */
81
- const useAnalyticsClientForTests = (analytics) => {
85
+ const useAnalyticsClientForTests = (analytics, testAnonymousId) => {
82
86
  client = analytics;
83
87
  clientInitialized = true;
88
+ anonymousId = testAnonymousId;
84
89
  };
85
90
  /**
86
91
  * Phase 1: Run before validation so the Segment client exists if any
@@ -97,7 +102,8 @@ const initAnalyticsClientMiddleware = (args) => {
97
102
  host: "https://track.neon.tech"
98
103
  });
99
104
  log.debug("Initialized CLI analytics client");
100
- client.identify({ userId: ANONYMOUS });
105
+ anonymousId = args.configDir ? getOrCreateAnonymousId(args.configDir) : void 0;
106
+ client.identify(analyticsIdentity(void 0, anonymousId));
101
107
  };
102
108
  /**
103
109
  * The attribution of the current attempt. A 401 retry runs the middleware again and starts a
@@ -179,9 +185,9 @@ const analyticsMiddleware = (args) => {
179
185
  })();
180
186
  queue = queue.then(() => lookup);
181
187
  enqueue((analytics, { userId }) => {
182
- analytics.identify({ userId: analyticsUserId(userId) });
188
+ analytics.identify(analyticsIdentity(userId, anonymousId));
183
189
  analytics.track({
184
- userId: analyticsUserId(userId),
190
+ ...analyticsIdentity(userId, anonymousId),
185
191
  event: "CLI Started",
186
192
  properties: startedProperties,
187
193
  context: { direct: true }
@@ -266,7 +272,7 @@ const sendError = (err, errCode) => {
266
272
  enqueue((analytics, { userId }) => {
267
273
  analytics.track({
268
274
  event: "CLI Error",
269
- userId: analyticsUserId(userId),
275
+ ...analyticsIdentity(userId, anonymousId),
270
276
  properties
271
277
  });
272
278
  });
@@ -284,7 +290,7 @@ const trackEvent = (event, properties) => {
284
290
  enqueue((analytics, { userId }) => {
285
291
  analytics.track({
286
292
  event,
287
- userId: analyticsUserId(userId),
293
+ ...analyticsIdentity(userId, anonymousId),
288
294
  properties
289
295
  });
290
296
  });
@@ -300,7 +306,7 @@ const trackCommandSuccess = (args) => {
300
306
  enqueue((analytics, { userId, accountId, authMethod, authData }) => {
301
307
  analytics.track({
302
308
  event: "cli_command_success",
303
- userId: analyticsUserId(userId),
309
+ ...analyticsIdentity(userId, anonymousId),
304
310
  properties: {
305
311
  ...properties,
306
312
  accountId,
@@ -348,4 +354,4 @@ const getAnalyticsEventProperties = (args) => ({
348
354
  githubEnvVars: getGithubEnvVars(process.env)
349
355
  });
350
356
  //#endregion
351
- export { analyticsMiddleware, analyticsUserId, closeAnalytics, commandSuccessProperties, getAnalyticsEventProperties, getErrorAnalyticsEventProperties, initAnalyticsClientMiddleware, recordCommandSuccessExtras, recordScaffoldedTemplate, sendError, storedCredentialAttribution, takeCommandSuccessExtras, telemetryCredential, trackCommandSuccess, trackEvent, useAnalyticsClientForTests };
357
+ export { analyticsIdentity, analyticsMiddleware, closeAnalytics, commandSuccessProperties, getAnalyticsEventProperties, getErrorAnalyticsEventProperties, initAnalyticsClientMiddleware, recordCommandSuccessExtras, recordScaffoldedTemplate, sendError, storedCredentialAttribution, takeCommandSuccessExtras, telemetryCredential, trackCommandSuccess, trackEvent, useAnalyticsClientForTests };
@@ -0,0 +1,59 @@
1
+ import { t as writeSecretFile } from "./_chunks/secure_file-BucZj4yQ.js";
2
+ import { isCi } from "./env.js";
3
+ import { log } from "./log.js";
4
+ import { isEexist } from "./refresh_lock.js";
5
+ import { linkSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { randomUUID } from "node:crypto";
8
+ //#region src/anonymous_id.ts
9
+ const ANONYMOUS_ID_FILE = "anonymous_id";
10
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11
+ const readStored = (path) => {
12
+ try {
13
+ const stored = readFileSync(path, "utf8").trim();
14
+ return UUID.test(stored) ? stored : void 0;
15
+ } catch {
16
+ return;
17
+ }
18
+ };
19
+ /**
20
+ * Names this installation in analytics before sign-in. Commands that know the user send it
21
+ * alongside `userId`, so earlier events can be linked to the account.
22
+ *
23
+ * Not stored in CI, where the config directory doesn't outlive the run and every run would
24
+ * mint a new installation. Call only once analytics is known to be enabled.
25
+ */
26
+ const getOrCreateAnonymousId = (configDir) => {
27
+ if (isCi()) return;
28
+ const path = join(configDir, ANONYMOUS_ID_FILE);
29
+ try {
30
+ const stored = readStored(path);
31
+ if (stored) return stored;
32
+ mkdirSync(configDir, { recursive: true });
33
+ const id = randomUUID();
34
+ const temporary = `${path}.${id}.tmp`;
35
+ writeFileSync(temporary, `${id}\n`, {
36
+ mode: 384,
37
+ flag: "wx"
38
+ });
39
+ try {
40
+ linkSync(temporary, path);
41
+ return id;
42
+ } catch (err) {
43
+ if (!isEexist(err)) throw err;
44
+ const winner = readStored(path);
45
+ if (winner) return winner;
46
+ writeSecretFile(path, `${id}\n`);
47
+ return id;
48
+ } finally {
49
+ try {
50
+ unlinkSync(temporary);
51
+ } catch {}
52
+ }
53
+ } catch (err) {
54
+ log.debug("Could not store the anonymous analytics id: %s", err);
55
+ return;
56
+ }
57
+ };
58
+ //#endregion
59
+ export { ANONYMOUS_ID_FILE, getOrCreateAnonymousId };
@@ -2,6 +2,7 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { a as OAUTH, i as API_KEY, l as interpretCredentials, s as credentialLabel, t as storeFor } from "../_chunks/credential_io-ByNuojoE.js";
3
3
  import { isCi } from "../env.js";
4
4
  import { log } from "../log.js";
5
+ import { withExclusiveLock } from "../refresh_lock.js";
5
6
  import { getApiClient, isNeonApiError } from "../api.js";
6
7
  import { setAuthContext } from "../auth_context.js";
7
8
  import { a as isOwnedCredentialPath } from "../_chunks/paths-DMq0Lt7a.js";
@@ -12,7 +13,6 @@ import { AuthRefreshError, auth, classifyRefreshFailure, refreshToken } from "..
12
13
  import { i as selectCredential, n as displacedProfileWarning, t as credentialInputs } from "../_chunks/auth_selection-YX5_KZgD.js";
13
14
  import { ClaimableClient, ClaimableServiceError } from "../claimable/api.js";
14
15
  import { assertionHasExpired, claimableCredentialsExist, claimableCredentialsPath, readLinkedClaimableCredentials, shouldUseClaimableCredentials } from "../claimable/state.js";
15
- import { withExclusiveLock } from "../refresh_lock.js";
16
16
  import { readOutgoingCredential, retirePreviousCredential } from "../retire_credential.js";
17
17
  import { getCliName } from "../utils/cli_name.js";
18
18
  import { helpEpilogue, helpWidth, wrapHelpText } from "../utils/help_text.js";
@@ -4,12 +4,12 @@ import { isClaimableEnvTarget } from "../claimable/state.js";
4
4
  import { getCliName } from "../utils/cli_name.js";
5
5
  import { defaultDevEnvFiles, readEnvFile, resolveEnvFilePath } from "../env_file.js";
6
6
  import { l as functionBaseUrlKey } from "../_chunks/env-BpP3Dqpe.js";
7
- import { branchIdResolve } from "../utils/enrichers.js";
8
- import { bundleEntry } from "../utils/esbuild.js";
9
7
  import { a as resolveDevEnv } from "../_chunks/env-BNm69Qh8.js";
10
- import { envFlag } from "./config.js";
11
8
  import { findConfigFunctionBySource, resolveFunctionsFromConfig } from "../dev/functions.js";
12
9
  import { resolveWatchInputs } from "../dev/inputs.js";
10
+ import { branchIdResolve } from "../utils/enrichers.js";
11
+ import { bundleEntry } from "../utils/esbuild.js";
12
+ import { envFlag } from "./config.js";
13
13
  import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
14
14
  import { basename, dirname, join, relative, resolve } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
@@ -7,9 +7,9 @@ import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOpt
7
7
  import { mergeEnvFile, readEnvFile, resolveEnvFilePath } from "../env_file.js";
8
8
  import { t as NEON_ENV_VAR_KEYS, u as isFunctionBaseUrlKey } from "../_chunks/env-BpP3Dqpe.js";
9
9
  import { ENV_PULL_KEY_HELP, ENV_PULL_SERVICES, envKeysForSelection, envPullFlagValue, ownedEnvServiceKeys, parseEnvPullKeys } from "../env_services.js";
10
+ import { i as dropClaimableUnsupported, o as resolveNeonEnvVars } from "../_chunks/env-BNm69Qh8.js";
10
11
  import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
11
12
  import { warnAiGateway } from "../utils/ai_gateway_notice.js";
12
- import { i as dropClaimableUnsupported, o as resolveNeonEnvVars } from "../_chunks/env-BNm69Qh8.js";
13
13
  import { formatPulledEnv, pulledBranch } from "./env_output.js";
14
14
  import { existsSync } from "node:fs";
15
15
  import chalk from "chalk";
@@ -1,6 +1,6 @@
1
1
  import { writer } from "../writer.js";
2
- import { branchIdFromProps } from "../utils/enrichers.js";
3
2
  import { o as resolveNeonEnvVars } from "../_chunks/env-BNm69Qh8.js";
3
+ import { branchIdFromProps } from "../utils/enrichers.js";
4
4
  import YAML from "yaml";
5
5
  //#region src/commands/subscribe.ts
6
6
  const builder = (argv) => argv.usage("$0 realtime subscribe \"<query>\" [options]").positional("query", {
@@ -1,7 +1,7 @@
1
1
  import { promises } from "node:fs";
2
2
  import * as path from "node:path";
3
- import * as os from "node:os";
4
3
  import { randomUUID } from "node:crypto";
4
+ import * as os from "node:os";
5
5
  //#region src/psql/io/history.ts
6
6
  /**
7
7
  * psql history file I/O.
@@ -41,4 +41,4 @@ const withExclusiveLock = async (lockPath, fn) => {
41
41
  }
42
42
  };
43
43
  //#endregion
44
- export { withExclusiveLock };
44
+ export { isEexist, withExclusiveLock };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "8.8.0",
3
+ "version": "8.9.0",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -70,10 +70,10 @@
70
70
  "yaml": "^2.9.0",
71
71
  "yargs": "17.7.2",
72
72
  "yoctocolors": "^2.1.2",
73
- "@neon/config": "1.9.2",
74
- "@neon/config-runtime": "1.10.1",
75
- "@neon/realtime": "0.1.1",
76
- "@neon/sdk": "7.3.0"
73
+ "@neon/config": "1.9.3",
74
+ "@neon/config-runtime": "1.10.2",
75
+ "@neon/realtime": "0.1.2",
76
+ "@neon/sdk": "7.4.0"
77
77
  },
78
78
  "optionalDependencies": {
79
79
  "@napi-rs/keyring": "1.3.0",
@@ -110,9 +110,9 @@
110
110
  "typescript": "^5.9.0",
111
111
  "vitest": "^3.0.9",
112
112
  "@neon-internals/cli-core": "0.0.0",
113
- "@neon-internals/env-core": "0.0.33",
113
+ "@neon-internals/env-core": "0.0.34",
114
114
  "@neon/e2e-harness": "0.0.0",
115
- "@neon/env": "1.6.3",
115
+ "@neon/env": "1.6.4",
116
116
  "@neon/functions": "0.11.0"
117
117
  },
118
118
  "publishConfig": {