mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 13:39:44 +02:00
feat(core): Add kind property to node type policy telemetry events (no-changelog) (#38749)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
b26bb7f452
commit
477481fec3
|
|
@ -16,6 +16,20 @@ const scope = z
|
|||
.enum(['instance', 'project'])
|
||||
.describe('Which scope the policy was written at; project policies compose under instance');
|
||||
|
||||
/**
|
||||
* Exported so callers that build these events from a domain event (which keeps `kind` as a
|
||||
* plain string, so a later kind needs no change there) can narrow it against the same set this
|
||||
* schema validates against, instead of redeclaring the list and risking drift.
|
||||
*/
|
||||
export const POLICY_KINDS = ['node-types', 'credential-types'] as const;
|
||||
export type PolicyKind = (typeof POLICY_KINDS)[number];
|
||||
|
||||
const kind = z
|
||||
.enum(POLICY_KINDS)
|
||||
.describe(
|
||||
'Which type family the policy governs. Credential type policies report through these same events instead of a second set, so this is what tells the two apart.',
|
||||
);
|
||||
|
||||
const projectId = z.string().optional().describe('Absent at instance scope');
|
||||
|
||||
const ruleCounts = {
|
||||
|
|
@ -35,6 +49,7 @@ export const NODE_TYPE_POLICIES_TELEMETRY = defineTelemetryEvents({
|
|||
properties: z.object({
|
||||
user_id: userId,
|
||||
source,
|
||||
kind,
|
||||
scope,
|
||||
project_id: projectId,
|
||||
default_action: z
|
||||
|
|
@ -80,6 +95,7 @@ export const NODE_TYPE_POLICIES_TELEMETRY = defineTelemetryEvents({
|
|||
properties: z.object({
|
||||
user_id: userId,
|
||||
source,
|
||||
kind,
|
||||
operation: z.enum(['created', 'updated', 'deleted']),
|
||||
policy_id: z.string(),
|
||||
...ruleCounts,
|
||||
|
|
@ -93,6 +109,7 @@ export const NODE_TYPE_POLICIES_TELEMETRY = defineTelemetryEvents({
|
|||
properties: z.object({
|
||||
user_id: userId,
|
||||
source,
|
||||
kind,
|
||||
scope,
|
||||
project_id: projectId,
|
||||
scope_id: z.string(),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ export type {
|
|||
TelemetryEventInput,
|
||||
TelemetryEventRegistry,
|
||||
} from './define';
|
||||
export { POLICY_KINDS } from './events/node-type-policies';
|
||||
export type { PolicyKind } from './events/node-type-policies';
|
||||
export { redactTelemetryProperties, redactTelemetryText } from './redaction';
|
||||
export type { TelemetryTextOptions } from './redaction';
|
||||
export { collectDuplicateNames, validateEntrySchemas } from './registry-checks';
|
||||
|
|
|
|||
|
|
@ -712,6 +712,7 @@ describe('TelemetryEventRelay', () => {
|
|||
{
|
||||
user_id: 'user123',
|
||||
source: 'user',
|
||||
kind: 'node-types',
|
||||
scope: 'instance',
|
||||
default_action: 'deny',
|
||||
previous_default_action: null,
|
||||
|
|
@ -735,6 +736,45 @@ describe('TelemetryEventRelay', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should report a credential type policy save with its own kind', () => {
|
||||
const event: RelayEventMap['node-type-policy-saved'] = {
|
||||
updatedBy: 'user123',
|
||||
kind: 'credential-types',
|
||||
projectId: null,
|
||||
scopeId: 'scope-1',
|
||||
before: null,
|
||||
after: { defaultAction: 'deny', version: 1 },
|
||||
rulesBefore: null,
|
||||
rulesAfter: [],
|
||||
warningCount: 0,
|
||||
};
|
||||
|
||||
eventService.emit('node-type-policy-saved', event);
|
||||
|
||||
expect(telemetry.track).toHaveBeenCalledWith(
|
||||
TELEMETRY_EVENT.NODE_TYPE_POLICIES.USER_SAVED_NODE_TYPE_POLICY,
|
||||
expect.objectContaining({ kind: 'credential-types' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should drop an unrecognized kind instead of reporting it', () => {
|
||||
const event: RelayEventMap['node-type-policy-saved'] = {
|
||||
updatedBy: 'user123',
|
||||
kind: 'not-a-registered-kind',
|
||||
projectId: null,
|
||||
scopeId: 'scope-1',
|
||||
before: null,
|
||||
after: { defaultAction: 'deny', version: 1 },
|
||||
rulesBefore: null,
|
||||
rulesAfter: [],
|
||||
warningCount: 0,
|
||||
};
|
||||
|
||||
eventService.emit('node-type-policy-saved', event);
|
||||
|
||||
expect(telemetry.track).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should track a project-scope save over an existing policy', () => {
|
||||
const event: RelayEventMap['node-type-policy-saved'] = {
|
||||
updatedBy: 'user123',
|
||||
|
|
@ -801,6 +841,7 @@ describe('TelemetryEventRelay', () => {
|
|||
{
|
||||
user_id: 'user123',
|
||||
source: 'user',
|
||||
kind: 'node-types',
|
||||
operation: 'created',
|
||||
policy_id: 'policy-1',
|
||||
rule_count: 1,
|
||||
|
|
@ -812,6 +853,37 @@ describe('TelemetryEventRelay', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should report a credential type policy document with its own kind', () => {
|
||||
const event: RelayEventMap['node-type-policy-document-created'] = {
|
||||
updatedBy: 'user123',
|
||||
kind: 'credential-types',
|
||||
policyId: 'policy-1',
|
||||
origin: 'document-api',
|
||||
after: { rules: [], version: 1 },
|
||||
};
|
||||
|
||||
eventService.emit('node-type-policy-document-created', event);
|
||||
|
||||
expect(telemetry.track).toHaveBeenCalledWith(
|
||||
TELEMETRY_EVENT.NODE_TYPE_POLICIES.USER_UPDATED_NODE_TYPE_POLICY_DOCUMENT,
|
||||
expect.objectContaining({ kind: 'credential-types' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should drop a policy document with an unrecognized kind instead of reporting it', () => {
|
||||
const event: RelayEventMap['node-type-policy-document-created'] = {
|
||||
updatedBy: 'user123',
|
||||
kind: 'not-a-registered-kind',
|
||||
policyId: 'policy-1',
|
||||
origin: 'document-api',
|
||||
after: { rules: [], version: 1 },
|
||||
};
|
||||
|
||||
eventService.emit('node-type-policy-document-created', event);
|
||||
|
||||
expect(telemetry.track).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should track an updated policy document', () => {
|
||||
const event: RelayEventMap['node-type-policy-document-updated'] = {
|
||||
updatedBy: 'user123',
|
||||
|
|
@ -1005,6 +1077,7 @@ describe('TelemetryEventRelay', () => {
|
|||
{
|
||||
user_id: 'user123',
|
||||
source: 'user',
|
||||
kind: 'node-types',
|
||||
scope: 'project',
|
||||
project_id: 'project-1',
|
||||
scope_id: 'scope-2',
|
||||
|
|
@ -1014,6 +1087,39 @@ describe('TelemetryEventRelay', () => {
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should report credential type policy attachments with their own kind', () => {
|
||||
const event: RelayEventMap['node-type-policy-attachments-updated'] = {
|
||||
updatedBy: 'user123',
|
||||
kind: 'credential-types',
|
||||
projectId: null,
|
||||
scopeId: 'scope-1',
|
||||
before: { attachments: [], version: 1 },
|
||||
after: { attachments: [], version: 2 },
|
||||
};
|
||||
|
||||
eventService.emit('node-type-policy-attachments-updated', event);
|
||||
|
||||
expect(telemetry.track).toHaveBeenCalledWith(
|
||||
TELEMETRY_EVENT.NODE_TYPE_POLICIES.USER_UPDATED_NODE_TYPE_POLICY_ATTACHMENTS,
|
||||
expect.objectContaining({ kind: 'credential-types' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should drop attachments with an unrecognized kind instead of reporting them', () => {
|
||||
const event: RelayEventMap['node-type-policy-attachments-updated'] = {
|
||||
updatedBy: 'user123',
|
||||
kind: 'not-a-registered-kind',
|
||||
projectId: null,
|
||||
scopeId: 'scope-1',
|
||||
before: { attachments: [], version: 1 },
|
||||
after: { attachments: [], version: 2 },
|
||||
};
|
||||
|
||||
eventService.emit('node-type-policy-attachments-updated', event);
|
||||
|
||||
expect(telemetry.track).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('external secrets events', () => {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from '@n8n/db';
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import { PROJECT_OWNER_ROLE_SLUG } from '@n8n/permissions';
|
||||
import { TELEMETRY_EVENT } from '@n8n/telemetry';
|
||||
import { POLICY_KINDS, TELEMETRY_EVENT, type PolicyKind } from '@n8n/telemetry';
|
||||
import { snakeCase } from 'change-case';
|
||||
import { BinaryDataConfig, InstanceSettings } from 'n8n-core';
|
||||
import type {
|
||||
|
|
@ -81,6 +81,18 @@ function policyScope(projectId: string | null): {
|
|||
return projectId === null ? { scope: 'instance' } : { scope: 'project', project_id: projectId };
|
||||
}
|
||||
|
||||
/**
|
||||
* The domain event keeps `kind` as a plain string, so a later ticket can add controllers for a
|
||||
* new kind (e.g. credential types) without changing the service or event map. Telemetry still
|
||||
* wants a closed set, so this narrows against the same `POLICY_KINDS` the schema validates
|
||||
* against — an unrecognized kind is a bug in the caller (a new kind landed without registering
|
||||
* it in the telemetry package), not something to crash telemetry over, so it is dropped rather
|
||||
* than reported.
|
||||
*/
|
||||
function isPolicyKind(kind: string): kind is PolicyKind {
|
||||
return (POLICY_KINDS as readonly string[]).includes(kind);
|
||||
}
|
||||
|
||||
function countRuleActions(rules: readonly PolicyRule[]) {
|
||||
return {
|
||||
rule_count: rules.length,
|
||||
|
|
@ -528,6 +540,7 @@ export class TelemetryEventRelay extends EventRelay {
|
|||
|
||||
private nodeTypePolicySaved({
|
||||
updatedBy,
|
||||
kind,
|
||||
projectId,
|
||||
before,
|
||||
after,
|
||||
|
|
@ -535,8 +548,11 @@ export class TelemetryEventRelay extends EventRelay {
|
|||
rulesAfter,
|
||||
warningCount,
|
||||
}: RelayEventMap['node-type-policy-saved']) {
|
||||
if (!isPolicyKind(kind)) return;
|
||||
|
||||
this.telemetry.track(TELEMETRY_EVENT.NODE_TYPE_POLICIES.USER_SAVED_NODE_TYPE_POLICY, {
|
||||
...policyActor(updatedBy),
|
||||
kind,
|
||||
...policyScope(projectId),
|
||||
default_action: after.defaultAction,
|
||||
previous_default_action: before?.defaultAction ?? null,
|
||||
|
|
@ -560,17 +576,19 @@ export class TelemetryEventRelay extends EventRelay {
|
|||
*/
|
||||
private nodeTypePolicyDocumentCreated({
|
||||
updatedBy,
|
||||
kind,
|
||||
policyId,
|
||||
origin,
|
||||
after,
|
||||
}: RelayEventMap['node-type-policy-document-created']) {
|
||||
if (origin === 'composed-save') return;
|
||||
|
||||
this.trackPolicyDocument(updatedBy, policyId, 'created', after.rules, null);
|
||||
this.trackPolicyDocument(updatedBy, kind, policyId, 'created', after.rules, null);
|
||||
}
|
||||
|
||||
private nodeTypePolicyDocumentUpdated({
|
||||
updatedBy,
|
||||
kind,
|
||||
policyId,
|
||||
origin,
|
||||
before,
|
||||
|
|
@ -578,28 +596,33 @@ export class TelemetryEventRelay extends EventRelay {
|
|||
}: RelayEventMap['node-type-policy-document-updated']) {
|
||||
if (origin === 'composed-save') return;
|
||||
|
||||
this.trackPolicyDocument(updatedBy, policyId, 'updated', after.rules, before.rules);
|
||||
this.trackPolicyDocument(updatedBy, kind, policyId, 'updated', after.rules, before.rules);
|
||||
}
|
||||
|
||||
private nodeTypePolicyDocumentDeleted({
|
||||
updatedBy,
|
||||
kind,
|
||||
policyId,
|
||||
before,
|
||||
}: RelayEventMap['node-type-policy-document-deleted']) {
|
||||
this.trackPolicyDocument(updatedBy, policyId, 'deleted', [], before.rules);
|
||||
this.trackPolicyDocument(updatedBy, kind, policyId, 'deleted', [], before.rules);
|
||||
}
|
||||
|
||||
private trackPolicyDocument(
|
||||
updatedBy: string,
|
||||
kind: string,
|
||||
policyId: string,
|
||||
operation: 'created' | 'updated' | 'deleted',
|
||||
rulesAfter: readonly PolicyRule[],
|
||||
rulesBefore: readonly PolicyRule[] | null,
|
||||
) {
|
||||
if (!isPolicyKind(kind)) return;
|
||||
|
||||
this.telemetry.track(
|
||||
TELEMETRY_EVENT.NODE_TYPE_POLICIES.USER_UPDATED_NODE_TYPE_POLICY_DOCUMENT,
|
||||
{
|
||||
...policyActor(updatedBy),
|
||||
kind,
|
||||
operation,
|
||||
policy_id: policyId,
|
||||
...countRuleActions(rulesAfter),
|
||||
|
|
@ -610,15 +633,19 @@ export class TelemetryEventRelay extends EventRelay {
|
|||
|
||||
private nodeTypePolicyAttachmentsUpdated({
|
||||
updatedBy,
|
||||
kind,
|
||||
projectId,
|
||||
scopeId,
|
||||
before,
|
||||
after,
|
||||
}: RelayEventMap['node-type-policy-attachments-updated']) {
|
||||
if (!isPolicyKind(kind)) return;
|
||||
|
||||
this.telemetry.track(
|
||||
TELEMETRY_EVENT.NODE_TYPE_POLICIES.USER_UPDATED_NODE_TYPE_POLICY_ATTACHMENTS,
|
||||
{
|
||||
...policyActor(updatedBy),
|
||||
kind,
|
||||
...policyScope(projectId),
|
||||
scope_id: scopeId,
|
||||
attachment_count: after.attachments.length,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user