diff --git a/.env b/.env
index a9992747..3c2e57ff 100644
--- a/.env
+++ b/.env
@@ -1,14 +1,16 @@
-#SECRETS
+# SECRETS
SECRET=REPLACE_ME
SALT=1234567890123456
VAULT_PWD=REPLACE_ME
-#MONGO
+# MONGO
DB_HOST=mongo
DB_NAME=ssm
DB_PORT=27017
-#REDIS
+# REDIS
REDIS_HOST=redis
REDIS_PORT=6379
-#SSM CONFIG
+# SSM CONFIG
#SSM_INSTALL_PATH=/opt/squirrelserversmanager
-#SSM_DATA_PATH=/data
\ No newline at end of file
+#SSM_DATA_PATH=/data
+# TELEMETRY
+TELEMETRY_ENABLED=true
\ No newline at end of file
diff --git a/.env.dev b/.env.dev
index a9992747..a6c06db5 100644
--- a/.env.dev
+++ b/.env.dev
@@ -1,14 +1,16 @@
-#SECRETS
+# SECRETS
SECRET=REPLACE_ME
SALT=1234567890123456
VAULT_PWD=REPLACE_ME
-#MONGO
+# MONGO
DB_HOST=mongo
DB_NAME=ssm
DB_PORT=27017
#REDIS
REDIS_HOST=redis
REDIS_PORT=6379
-#SSM CONFIG
+# SSM CONFIG
#SSM_INSTALL_PATH=/opt/squirrelserversmanager
-#SSM_DATA_PATH=/data
\ No newline at end of file
+#SSM_DATA_PATH=/data
+# TELEMETRY
+TELEMETRY_ENABLED=true
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 1375ec54..2200d192 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -24,6 +24,10 @@
+
+
+
+
@@ -56,5 +60,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index a22aafe6..1850f6fa 100644
--- a/README.md
+++ b/README.md
@@ -70,6 +70,13 @@ See [Troubleshoot](https://squirrelserversmanager.io/docs/technical-guide/troubl
![Device Info](./site/public/home/device-info.png)
![New Device](./site/public/home/new-device.png)
+---
+## Disabling Anonymized Telemetry
+
+By default, SSM automatically reports anonymized basic usage statistics. This helps us understand how SSM is used and track its overall usage and growth. This data does not include any sensitive information. To disable anonymized telemetry, follow these steps:
+
+Set `TELEMETRY_ENABLED` to `false` in your `.env` file.
+
---
**Note:**
diff --git a/server/package-lock.json b/server/package-lock.json
index 4894e1f7..a2501c44 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ssm-server",
- "version": "0.1.25",
+ "version": "0.1.26",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ssm-server",
- "version": "0.1.25",
+ "version": "0.1.26",
"license": "AGPL-3.0 license",
"dependencies": {
"@aws-sdk/client-ecr": "^3.699.0",
@@ -43,6 +43,7 @@
"pino-http": "^10.3.0",
"pino-mongodb": "^4.3.0",
"pino-pretty": "^13.0.0",
+ "posthog-node": "^4.3.2",
"redis": "^4.7.0",
"rewire": "^7.0.0",
"semver": "^7.6.3",
@@ -98,7 +99,7 @@
},
"../shared-lib": {
"name": "ssm-shared-lib",
- "version": "0.1.25",
+ "version": "0.1.26",
"license": "AGPL-3.0 license",
"dependencies": {
"typescript": "^5.7.2"
@@ -8423,6 +8424,19 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/posthog-node": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.3.2.tgz",
+ "integrity": "sha512-vy8Mt9IEfniUgqQ1rOCQ31CBO1VNqDGd3ZtHlWR9/YfU6RiuK+9pUXPb4h6HTGzQmjL8NFnjd8K8NMXSX8S6MQ==",
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.7.4",
+ "rusha": "^0.8.14"
+ },
+ "engines": {
+ "node": ">=15.0.0"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -8985,6 +8999,12 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rusha": {
+ "version": "0.8.14",
+ "resolved": "https://registry.npmjs.org/rusha/-/rusha-0.8.14.tgz",
+ "integrity": "sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==",
+ "license": "MIT"
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
diff --git a/server/package.json b/server/package.json
index 224aa2ef..7d690ff6 100644
--- a/server/package.json
+++ b/server/package.json
@@ -56,7 +56,8 @@
"socket.io": "^4.8.1",
"multer": "^1.4.5-lts.1",
"js-yaml": "^4.1.0",
- "passport-mock-strategy": "^2.0.0"
+ "passport-mock-strategy": "^2.0.0",
+ "posthog-node": "^4.3.2"
},
"overrides": {
"minimatch": "5.1.2",
diff --git a/server/src/config.ts b/server/src/config.ts
index 45cd1715..da88c2aa 100644
--- a/server/src/config.ts
+++ b/server/src/config.ts
@@ -18,3 +18,4 @@ export const VAULT_PWD = process.env.VAULT_PWD || '';
export const SESSION_DURATION = parseInt(process.env.SESSION_DURATION || '86400000');
export const SSM_INSTALL_PATH = process.env.SSM_INSTALL_PATH || '/opt/squirrelserversmanager';
export const SSM_DATA_PATH = process.env.SSM_DATA_PATH || '/data';
+export const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED === 'true';
diff --git a/server/src/controllers/rest/user/login.ts b/server/src/controllers/rest/user/login.ts
index 8f19fe86..58f2a287 100644
--- a/server/src/controllers/rest/user/login.ts
+++ b/server/src/controllers/rest/user/login.ts
@@ -4,9 +4,11 @@ import { SECRET, SESSION_DURATION } from '../../../config';
import UserRepo from '../../../data/database/repository/UserRepo';
import { AuthFailureError } from '../../../middlewares/api/ApiError';
import { SuccessResponse } from '../../../middlewares/api/ApiResponse';
+import Telemetry from '../../../modules/telemetry';
export const login = async (req, res, next) => {
const { password, username } = req.body;
+ Telemetry.capture('user login');
if (!password || !username) {
res.status(401).send({
data: {
diff --git a/server/src/controllers/rest/user/user.ts b/server/src/controllers/rest/user/user.ts
index 8201fa96..45dd2207 100644
--- a/server/src/controllers/rest/user/user.ts
+++ b/server/src/controllers/rest/user/user.ts
@@ -9,6 +9,7 @@ import logger from '../../../logger';
import { AuthFailureError } from '../../../middlewares/api/ApiError';
import { SuccessResponse } from '../../../middlewares/api/ApiResponse';
import { createADefaultLocalUserRepository } from '../../../modules/repository/default-playbooks-repositories';
+import Telemetry from '../../../modules/telemetry';
import DashboardUseCase from '../../../services/DashboardUseCase';
import DeviceUseCases from '../../../services/DeviceUseCases';
@@ -99,6 +100,7 @@ export const getCurrentUser = async (req, res) => {
export const createFirstUser = async (req, res) => {
const { email, password, name, avatar } = req.body;
+ Telemetry.capture('user signed up');
const hasUser = (await UserRepo.count()) > 0;
if (hasUser) {
throw new AuthFailureError('Your instance already has a user, you must first connect');
diff --git a/server/src/core/startup/index.ts b/server/src/core/startup/index.ts
index 138caf79..228fc661 100644
--- a/server/src/core/startup/index.ts
+++ b/server/src/core/startup/index.ts
@@ -19,6 +19,7 @@ import ContainerCustomStacksRepositoryEngine from '../../modules/repository/Cont
import { createADefaultLocalUserRepository } from '../../modules/repository/default-playbooks-repositories';
import PlaybooksRepositoryEngine from '../../modules/repository/PlaybooksRepositoryEngine';
import sshPrivateKeyFileManager from '../../modules/shell/managers/SshPrivateKeyFileManager';
+import Telemetry from '../../modules/telemetry';
import UpdateChecker from '../../modules/update/UpdateChecker';
import ContainerRegistryUseCases from '../../services/ContainerRegistryUseCases';
import { setAnsibleVersions } from '../system/ansible-versions';
@@ -51,10 +52,21 @@ class Startup {
void AutomationEngine.init();
void UpdateChecker.checkVersion();
void ContainerCustomStacksRepositoryEngine.init();
+ void Telemetry.init();
}
private async updateScheme() {
this.logger.warn('updateScheme - Scheme version differed, starting applying updates...');
+
+ try {
+ const installId = await getFromCache(SettingsKeys.GeneralSettingsKeys.INSTALL_ID);
+ if (!installId) {
+ await setToCache(SettingsKeys.GeneralSettingsKeys.INSTALL_ID, uuidv4());
+ }
+ } catch (error: any) {
+ this.logger.error(`Error settings installId: ${error.message}`);
+ }
+
try {
await PlaybookModel.syncIndexes();
this.logger.info('PlaybookModel indexes synchronized successfully.');
diff --git a/server/src/index.ts b/server/src/index.ts
index 322691ed..64b70a9e 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -5,6 +5,7 @@ import Startup from './core/startup';
import './middlewares/Passport';
import Crons from './modules/crons';
import app from './App';
+import Telemetry from './modules/telemetry';
const start = () => {
logger.info(`
@@ -34,6 +35,9 @@ export const restart = async () => {
app.stopServer(start);
};
+process.on('SIGINT', Telemetry.shutdown);
+process.on('SIGTERM', Telemetry.shutdown);
+
/*process.on('uncaughtException', (err, origin) => {
console.error('Unhandled exception. Please handle!', err.stack || err);
console.error(`Origin: ${JSON.stringify(origin)}`);
diff --git a/server/src/modules/telemetry/index.ts b/server/src/modules/telemetry/index.ts
new file mode 100644
index 00000000..6eb8d7af
--- /dev/null
+++ b/server/src/modules/telemetry/index.ts
@@ -0,0 +1,42 @@
+import { PostHog } from 'posthog-node';
+import { SettingsKeys } from 'ssm-shared-lib';
+import { TELEMETRY_ENABLED } from '../../config';
+import { getFromCache } from '../../data/cache';
+import logger from '../../logger';
+
+class Telemetry {
+ private client;
+ private _id!: string;
+
+ constructor() {
+ this.client = new PostHog('phc_wJKUU2ssGzXxFferOrilvhErmTxvx8jZCf77PCW24JG', {
+ host: 'https://us.i.posthog.com',
+ });
+ }
+
+ public capture(eventName: string) {
+ if (TELEMETRY_ENABLED) {
+ this.client?.capture({ distinctId: this._id, event: eventName });
+ }
+ }
+
+ public async init() {
+ const installId = await getFromCache(SettingsKeys.GeneralSettingsKeys.INSTALL_ID);
+ if (!installId) {
+ logger.error('Install ID not found');
+ }
+ if (installId) {
+ this._id = installId;
+ this.client?.identify({ distinctId: this._id });
+ }
+ if (!TELEMETRY_ENABLED) {
+ await this.client?.optOut();
+ }
+ }
+
+ public async shutdown() {
+ await this.client?.shutdown();
+ }
+}
+
+export default new Telemetry();
diff --git a/shared-lib/src/enums/settings.ts b/shared-lib/src/enums/settings.ts
index 09c62d00..c03115e4 100644
--- a/shared-lib/src/enums/settings.ts
+++ b/shared-lib/src/enums/settings.ts
@@ -9,10 +9,11 @@ export enum GeneralSettingsKeys {
DEVICE_STATS_RETENTION_IN_DAYS = 'device-stats-retention-in-days',
CONTAINER_STATS_RETENTION_IN_DAYS = 'container-stats-retention-in-days',
UPDATE_AVAILABLE = 'update-available',
+ INSTALL_ID = 'install-id',
}
export enum DefaultValue {
- SCHEME_VERSION = '17',
+ SCHEME_VERSION = '18',
SERVER_LOG_RETENTION_IN_DAYS = '30',
CONSIDER_DEVICE_OFFLINE_AFTER_IN_MINUTES = '3',
CONSIDER_PERFORMANCE_GOOD_MEM_IF_GREATER = '10',
diff --git a/site/docs/install/dockerless.md b/site/docs/install/dockerless.md
index 169da2d6..c55b11e6 100644
--- a/site/docs/install/dockerless.md
+++ b/site/docs/install/dockerless.md
@@ -150,6 +150,8 @@ REDIS_PORT=6379
# SSM CONFIG
SSM_INSTALL_PATH=/opt/squirrelserversmanager
SSM_DATA_PATH=/opt/squirrelserversmanager/data
+# TELEMETRY
+TELEMETRY_ENABLED=true
EOF
```
diff --git a/site/docs/quickstart.md b/site/docs/quickstart.md
index ba4ba57e..2ab23418 100644
--- a/site/docs/quickstart.md
+++ b/site/docs/quickstart.md
@@ -103,6 +103,8 @@ DB_PORT=27017
#REDIS
REDIS_HOST=redis
REDIS_PORT=6379
+#TELEMETRY
+TELEMETRY_ENABLED=true
```
Replace the values of "SECRET", "SALT", and "VAULT_PWD"
@@ -125,4 +127,11 @@ docker compose up
---
### Other install methods:
-To manually build the project your self, see this [section](/docs/technical-guide/manual-install-ssm)
\ No newline at end of file
+To manually build the project your self, see this [section](/docs/technical-guide/manual-install-ssm)
+
+---
+### Disabling Anonymized Telemetry
+
+By default, SSM automatically reports anonymized basic usage statistics. This helps us understand how SSM is used and track its overall usage and growth. This data does not include any sensitive information. To disable anonymized telemetry, follow these steps:
+
+Set `TELEMETRY_ENABLED` to `false` in your `.env` file.