countly-server

GitHub

Countly is a privacy-first, AI-powered analytics and engagement platform for understanding and optimizing customer journeys across digital applications, from desktop and mobile to IoT and connected environments.

RAW Rules

CLAUDE.md

# CLAUDE.md - Countly Server

This file provides guidance for Claude (Anthropic) when working with this codebase.

## Project Overview

Countly is a product analytics platform. Backend: Node.js 22+, MongoDB. Frontend: Vue 2, Element UI, Backbone (legacy). Architecture is plugin-based.

## Quick Commands

```bash
npm run start:all:dev        # Start all services (API:3001, Frontend:6001)
npx grunt dist-all           # Build static assets (required after JS changes)
npx grunt locales            # Build locale files
npx grunt sass               # Compile SASS only

# Testing
npm run test:unit            # Unit tests
npm run test:plugin -- name  # Single plugin tests
countly plugin lint name     # Lint plugin
countly shellcheck           # Validate shell scripts
```

## Critical Security Rules

**ALWAYS follow these - no exceptions:**

1. **API endpoints must use validation**:
   ```javascript
   const { validateRead, validateCreate, validateUpdate, validateDelete } = require('../../../api/utils/rights.js');
   validateRead(params, FEATURE_NAME, () => { /* handler */ });
   ```

2. **Write operations must include app_id**:
   ```javascript
   // CORRECT - prevents cross-app access
   db.collection("items").deleteOne({_id: id, app_id: params.app_id + ""});
   ```

3. **Cast user input to strings for auth**:
   ```javascript
   params.username = params.username + "";
   ```

4. **Use spawn, not exec for shell commands**:
   ```javascript
   cp.spawn("command", [userInput]);  // Safe
   // exec("command " + userInput);   // VULNERABLE
   ```

5. **Never use v-html with user data** in Vue templates.

6. **Validate user-supplied Mongo queries — reject, never strip**. Any query/filter that comes from a request and reaches `find`/`aggregate`/`update`/`delete` must be checked with the `common` helpers at the endpoint where it is first parsed (NOT inside deep helpers or the `/drill/preprocess_query` hook). The query is run exactly as submitted or the request is rejected with `400` — it is never modified.
   ```javascript
   // raw query STRING from a request param → parse + validate in one step
   var parsed = common.parseUserQuery(params.qstring.query); // accepts string OR object
   if (parsed.error) {
       log.d("Rejected user query" + common.reqInfo(params) + ": " + parsed.error);
       return common.returnMessage(params, 400, parsed.error);
   }
   var query = parsed.query; // safe to run as-is

   // ALREADY-parsed object — e.g. dbviewer parses with EJSON, or the query is
   // nested in a larger saved payload. Validate that parsed object directly:
   var parsedQuery = EJSON.parse(params.qstring.filter); // example: already parsed (EJSON / stored doc)
   var badOp = common.findUnsafeMongoOperator(parsedQuery);
   if (badOp) {
       log.d("Rejected user query" + common.reqInfo(params) + ": Query contains disallowed operator: " + badOp);
       return common.returnMessage(params, 400, "Query contains disallowed operator: " + badOp);
   }
   ```
   `$expr` is allowed; `$where`/`$function`/`$accumulator` are rejected at any depth (including nested inside `$expr`). Log the rejection at the call site using the file's `log` and `common.reqInfo(params)` (which adds the endpoint path/method, without the api_key). Do NOT pass `params` into `parseUserQuery` and do NOT log inside it.

## File Locations

| What | Where |
|------|-------|
| Plugin code | `plugins/<name>/api/api.js` |
| Vue views | `plugins/<name>/frontend/public/javascripts/countly.views.js` |
| Templates | `plugins/<name>/frontend/public/templates/` |
| Localization | `plugins/<name>/frontend/public/localization/<name>.properties` |
| Tests | `plugins/<name>/tests.js` |

## Creating API Endpoints

```javascript
var plugins = require('../../pluginManager.js');
var common = require('../../../api/utils/common.js');
const { validateRead } = require('../../../api/utils/rights.js');

const FEATURE_NAME = 'myfeature';

plugins.register("/o/myfeature", function(ob) {
    var params = ob.params;
    validateRead(params, FEATURE_NAME, function() {
        // Validate input
        var argProps = {
            'id': { 'required': true, 'type': 'String' }
        };
        var validation = common.validateArgs(params.qstring, argProps, true);
        if (!validation.obj) {
            common.returnMessage(params, 400, 'Error: ' + validation.errors);
            return;
        }
        
        // Query with app_id
        common.db.collection('mydata').findOne(
            {_id: validation.obj.id, app_id: params.app_id + ""},
            function(err, result) {
                common.returnOutput(params, result || {});
            }
        );
    });
});
```

## Creating Vue Components

```javascript
var MyComponent = countlyVue.views.create({
    template: countlyVue.T("/myplugin/templates/main.html"),
    mixins: [countlyVue.mixins.auth(FEATURE_NAME)],
    data: function() {
        return { items: [] };
    },
    computed: {
        // Prefer computed over watchers
    },
    methods: {
        refresh: function() {
            // Called for auto-refresh
        }
    }
});

app.route('/dashboard/myfeature', 'myfeature', function() {
    new countlyVue.views.BackboneWrapper({ component: MyComponent }).render();
});
```

## MongoDB Patterns

```javascript
// Read batcher for hot documents
common.readBatcher.getOne("collection", {_id: id}, callback);

// Write batcher for multiple updates
common.writeBatcher.add("collection", id, {$inc: {count: 1}});

// Always use projection
db.collection('x').findOne({_id: id}, {projection: {field: 1}});
```

## Plugin Lifecycle Hooks

```javascript
// Required if your plugin creates per-app collections
plugins.register("/i/apps/create", function(ob) {
    common.db.collection('mydata' + ob.appId).createIndex({"field": 1});
});

plugins.register("/i/apps/delete", function(ob) {
    common.db.collection('mydata' + ob.appId).drop();
});

plugins.register("/i/app_users/delete", function(ob) {
    common.db.collection("mydata" + ob.app_id).deleteMany({uid: {$in: ob.uids}});
});
```

## CSS/Styling

- Use SASS with SCSS syntax
- BEM naming with `cly-vue-` prefix
- Bulma classes use `bu-` prefix
- Use `@use`, never `@import`

## Common Anti-Patterns to Avoid

| Don't | Do Instead |
|-------|------------|
| `this.$parent.value = x` | Emit events: `this.$emit('update', x)` |
| Deep watchers | Watch specific properties |
| `v-html` with user data | Use `{{ }}` text interpolation |
| Query without app_id | Always include `app_id` in queries |
| `exec(cmd + userInput)` | `spawn(cmd, [userInput])` |
| `replace(' ', '-')` | `replace(/ /g, '-')` for all occurrences |

## Detailed Documentation

For comprehensive guidelines, read:
- `CODING_GUIDELINES.md` - Full development standards
- `docs/SECURITY.md` - Security requirements
- `docs/VUEJS_GUIDELINES.md` - Vue patterns
- `docs/CSS_STYLE_GUIDE.md` - SASS/BEM conventions
- `docs/UI_TESTING.md` - Cypress testing
- `test/README.md` - Test suite documentation
- `plugins/empty/` - Sample plugin structure