Skip to content

Conversation

@cteyton
Copy link
Contributor

@cteyton cteyton commented Jan 5, 2026

Send server version and persistent instanceId to n8n webhook on first startup. Skips on subsequent restarts (cached) and in CI environments.

⚠️ Don't keep track of any personal information such as IP address or hostname. Only the instanceId and the app version to get insights of our usage.

Explanation

This PR adds a startup ping mechanism that sends server telemetry to an n8n webhook when the Packmind API server starts for the first time. The ping includes:

  • Server version (from root package.json)
  • Persistent instanceId (UUID stored in Redis cache with 365-day expiration)

The ping is sent only once on the first startup. Subsequent server restarts skip the ping by checking if an instanceId already exists in the cache. The ping is also disabled in CI environments (CI=true) and can be manually disabled via DISABLE_PING=true.

Relates to #

Type of Change

  • Bug fix
  • New feature
  • Improvement/Enhancement
  • Refactoring
  • Documentation
  • Breaking change

Affected Components

  • Domain packages affected: None
  • Frontend / Backend / Both: Backend only (apps/api)
  • Breaking changes (if any): None

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing completed
  • Test coverage maintained or improved

Test Details:

Manual testing scenarios:

  1. First server startup - ping should be sent with new instanceId
  2. Subsequent server restarts - ping should be skipped (instanceId exists in cache)
  3. CI environment (CI=true) - ping should be skipped
  4. DISABLE_PING=true - ping should be skipped
  5. Cache unavailable - ping should be skipped gracefully without crashing server
  6. Webhook unreachable - server should start successfully, error logged

Reviewer Notes

Design Decisions:

  • Early return if instanceId exists in cache (avoids redundant checks on restart)
  • 365-day cache expiration to maintain same instanceId for a year
  • Uses existing Cache and Configuration services from @packmind/node-utils
  • axios and uuid already in dependencies (docker-package.json)

Send server version and persistent instanceId to n8n webhook on first startup.
Skips on subsequent restarts (cached) and in CI environments.
Send server version and instanceId to n8n webhook on first startup or when version changes.
Tracks both values in separate cache keys to detect upgrades while maintaining same instanceId.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a startup telemetry mechanism that sends a one-time ping to an n8n webhook containing the server version and a persistent instanceId. The ping is designed to execute only on the first server startup, with subsequent restarts being skipped by checking for an existing instanceId in Redis cache.

Key Changes:

  • Added startup ping function with version detection from package.json
  • Integrated cache-based deduplication using 365-day expiration
  • Implemented environment-based opt-out (CI environments and DISABLE_PING flag)

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

File Description
apps/api/src/startup/ping-packmind-setup.ts New module implementing the startup ping logic with version detection, cache-based deduplication, and webhook communication
apps/api/src/main.ts Integrated pingPackmindSetup call after server startup with fire-and-forget error handling

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +100 to +107
try {
const cache = Cache.getInstance();

// Check if ping is disabled via environment variable
const disablePing = await Configuration.getConfig('DISABLE_PING');
if (disablePing === 'true') {
logger.info('Ping disabled via DISABLE_PING environment variable');
return;
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a potential race condition in this implementation. If multiple server instances start simultaneously (common in container orchestration like Kubernetes), they could all check the cache at line 102 before any has written to it, resulting in multiple pings being sent with different instanceIds. Consider using a distributed lock or atomic cache operation (like Redis SETNX) to ensure only one instance generates and stores the instanceId.

Copilot uses AI. Check for mistakes.
Comment on lines +137 to +144
await cache.set(
INSTANCE_ID_CACHE_KEY,
newInstanceId,
CACHE_EXPIRATION_SECONDS,
);
await cache.set(
VERSION_CACHE_KEY,
currentVersion,
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instanceId is being stored in cache before verifying that the webhook ping was successful. If the webhook call fails (lines 144), the instanceId remains in cache and prevents retries on subsequent startups. Consider storing the instanceId only after a successful ping response, or implement a separate flag to track successful ping completion.

Copilot uses AI. Check for mistakes.
}
} catch {
// Continue to next path
continue;
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch block at line 40 uses an empty catch with continue, but the continue statement is unnecessary since it's the last statement in the try-catch block within the loop. The loop would naturally continue to the next iteration. Consider removing the explicit continue for cleaner code.

Suggested change
continue;

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +148
pingPackmindSetup().catch((error) => {
logger.warn('Ping to Packmind setup webhook failed', {
error: error instanceof Error ? error.message : String(error),
});
});
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling is redundant. The pingPackmindSetup function already has comprehensive try-catch blocks that prevent any errors from propagating (lines 145-149 in ping-packmind-setup.ts). This outer catch block in main.ts will never execute because pingPackmindSetup never throws. Consider removing this redundant error handler or simplifying the error handling strategy.

Suggested change
pingPackmindSetup().catch((error) => {
logger.warn('Ping to Packmind setup webhook failed', {
error: error instanceof Error ? error.message : String(error),
});
});
pingPackmindSetup();

Copilot uses AI. Check for mistakes.
const INSTANCE_ID_CACHE_KEY = 'packmind:instanceId';
const VERSION_CACHE_KEY = 'packmind:version';
const CACHE_EXPIRATION_SECONDS = 365 * 24 * 60 * 60; // 365 days
const WEBHOOK_URL =
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The webhook URL is hardcoded, which makes it difficult to test in different environments (development, staging, production) and prevents users from pointing to their own telemetry endpoints if desired. Consider making the webhook URL configurable via environment variable (e.g., PING_WEBHOOK_URL) with this URL as the default fallback.

Suggested change
const WEBHOOK_URL =
const WEBHOOK_URL =
process.env.PING_WEBHOOK_URL ||

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +82

logger.info('Sending ping to Packmind setup webhook', {
url: WEBHOOK_URL,
version,
instanceId,
});

await axios.post(WEBHOOK_URL, payload, {
timeout: 10000, // 10 second timeout
headers: {
'Content-Type': 'application/json',
},
});

logger.info('Successfully sent ping to Packmind setup webhook', {
version,
instanceId,
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instanceId is being logged at INFO level in multiple places (lines 66-70, 79-82, 132-134), which could expose this identifier in production logs. Since this is meant to be a persistent identifier for tracking, consider logging it at DEBUG level instead to reduce exposure in production log aggregation systems, or ensure that your logging infrastructure properly handles sensitive identifiers.

Copilot uses AI. Check for mistakes.
const possiblePaths = [
path.join(process.cwd(), 'package.json'), // Current working directory (works in most cases)
path.join(__dirname, '..', '..', '..', '..', 'package.json'), // From apps/api/src/startup to root
path.join(__dirname, 'package.json'), // In dist/apps/api (production build)
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 22 mentions "dist/apps/api/package.json (copied during build)" but there's no corresponding path in the possiblePaths array that checks for dist/apps/api/package.json. Path 3 checks "__dirname/package.json" which would only work if __dirname is dist/apps/api, but this is not explicitly documented. Consider adding an explicit path like "path.join(__dirname, '..', '..', 'package.json')" to match the comment, or update the comment to accurately reflect the paths being checked.

Suggested change
path.join(__dirname, 'package.json'), // In dist/apps/api (production build)
path.join(__dirname, '..', '..', 'package.json'), // In dist/apps/api (production build)

Copilot uses AI. Check for mistakes.
@sonarqubecloud
Copy link

sonarqubecloud bot commented Jan 5, 2026

@vincent-psarga vincent-psarga merged commit 1a8ec8c into main Jan 7, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants