diff --git a/docs/test.ts b/docs/test.ts index c2a198d7d98..677c4cb4deb 100644 --- a/docs/test.ts +++ b/docs/test.ts @@ -460,6 +460,9 @@ tracer.appsec.trackUserLoginFailureEvent('user_id', true, meta) tracer.appsec.trackUserLoginFailureEvent('user_id', false) tracer.appsec.trackUserLoginFailureEvent('user_id', false, meta) +tracer.appsec.trackUserSignupEvent('user_id') +tracer.appsec.trackUserSignupEvent('user_id', meta) + tracer.appsec.trackCustomEvent('event_name') tracer.appsec.trackCustomEvent('event_name', meta) diff --git a/index.d.ts b/index.d.ts index b26ea933b8d..7fee505f746 100644 --- a/index.d.ts +++ b/index.d.ts @@ -769,7 +769,7 @@ declare namespace tracer { */ maxDepth?: number } - + /** * Configuration enabling LLM Observability. Enablement is superceded by the DD_LLMOBS_ENABLED environment variable. */ @@ -888,6 +888,15 @@ declare namespace tracer { */ trackUserLoginFailureEvent(userId: string, exists: boolean, metadata?: { [key: string]: string }): void + /** + * Links a signup event to the current trace. + * @param {string} userId The user id of the signed-up user + * @param {[key: string]: string} metadata Custom fields to link to the signup event. + * + * @beta This method is in beta and could change in future versions. + */ + trackUserSignupEvent(userId: string, metadata?: { [key: string]: string }): void + /** * Links a custom event to the current trace. * @param {string} eventName The name of the event. @@ -2238,7 +2247,7 @@ declare namespace tracer { * Disable LLM Observability tracing. */ disable (): void, - + /** * Instruments a function by automatically creating a span activated on its * scope. @@ -2280,10 +2289,10 @@ declare namespace tracer { /** * Decorate a function in a javascript runtime that supports function decorators. * Note that this is **not** supported in the Node.js runtime, but is in TypeScript. - * + * * In TypeScript, this decorator is only supported in contexts where general TypeScript * function decorators are supported. - * + * * @param options Optional LLM Observability span options. */ decorate (options: llmobs.LLMObsNamelessSpanOptions): any @@ -2300,7 +2309,7 @@ declare namespace tracer { /** * Sets inputs, outputs, tags, metadata, and metrics as provided for a given LLM Observability span. * Note that with the exception of tags, this method will override any existing values for the provided fields. - * + * * For example: * ```javascript * llmobs.trace({ kind: 'llm', name: 'myLLM', modelName: 'gpt-4o', modelProvider: 'openai' }, () => { @@ -2313,7 +2322,7 @@ declare namespace tracer { * }) * }) * ``` - * + * * @param span The span to annotate (defaults to the current LLM Observability span if not provided) * @param options An object containing the inputs, outputs, tags, metadata, and metrics to set on the span. */ @@ -2489,14 +2498,14 @@ declare namespace tracer { * LLM Observability span kind. One of `agent`, `workflow`, `task`, `tool`, `retrieval`, `embedding`, or `llm`. */ kind: llmobs.spanKind, - + /** * The ID of the underlying user session. Required for tracking sessions. */ sessionId?: string, /** - * The name of the ML application that the agent is orchestrating. + * The name of the ML application that the agent is orchestrating. * If not provided, the default value will be set to mlApp provided during initalization, or `DD_LLMOBS_ML_APP`. */ mlApp?: string, diff --git a/packages/dd-trace/src/appsec/addresses.js b/packages/dd-trace/src/appsec/addresses.js index e02f1a5018d..4d8ff95ad86 100644 --- a/packages/dd-trace/src/appsec/addresses.js +++ b/packages/dd-trace/src/appsec/addresses.js @@ -32,5 +32,6 @@ module.exports = { DB_SYSTEM: 'server.db.system', LOGIN_SUCCESS: 'server.business_logic.users.login.success', - LOGIN_FAILURE: 'server.business_logic.users.login.failure' + LOGIN_FAILURE: 'server.business_logic.users.login.failure', + SIGNUP: 'server.business_logic.users.signup' } diff --git a/packages/dd-trace/src/appsec/sdk/index.js b/packages/dd-trace/src/appsec/sdk/index.js index 447d7b1121a..311e1484f97 100644 --- a/packages/dd-trace/src/appsec/sdk/index.js +++ b/packages/dd-trace/src/appsec/sdk/index.js @@ -1,6 +1,6 @@ 'use strict' -const { trackUserLoginSuccessEvent, trackUserLoginFailureEvent, trackCustomEvent } = require('./track_event') +const { trackUserLoginSuccessEvent, trackUserLoginFailureEvent, trackUserSignupEvent, trackCustomEvent } = require('./track_event') const { checkUserAndSetUser, blockRequest } = require('./user_blocking') const { setTemplates } = require('../blocking') const { setUser } = require('./set_user') @@ -21,6 +21,10 @@ class AppsecSdk { return trackUserLoginFailureEvent(this._tracer, userId, exists, metadata) } + trackUserSignupEvent (userId, metadata) { + return trackUserSignupEvent(this._tracer, userId, metadata) + } + trackCustomEvent (eventName, metadata) { return trackCustomEvent(this._tracer, eventName, metadata) } diff --git a/packages/dd-trace/src/appsec/sdk/track_event.js b/packages/dd-trace/src/appsec/sdk/track_event.js index 876dd0a8f15..3cdc07e454c 100644 --- a/packages/dd-trace/src/appsec/sdk/track_event.js +++ b/packages/dd-trace/src/appsec/sdk/track_event.js @@ -41,6 +41,26 @@ function trackUserLoginFailureEvent (tracer, userId, exists, metadata) { trackEvent('users.login.failure', fields, 'trackUserLoginFailureEvent', getRootSpan(tracer), 'sdk') } +function trackUserSignupEvent (tracer, userId, metadata) { + const rootSpan = getRootSpan(tracer) + if (!rootSpan) { + log.warn('Root span not available in trackUserSignupEvent') + return + } + + if (!userId || typeof userId !== 'string') { + log.warn('Invalid userId provided to trackUserSignupEvent') + return + } + + const fields = { + 'usr.id': userId, + ...metadata + } + + trackEvent('users.signup', fields, 'trackUserSignupEvent', rootSpan, 'sdk') +} + function trackCustomEvent (tracer, eventName, metadata) { if (!eventName || typeof eventName !== 'string') { log.warn('Invalid eventName provided to trackCustomEvent') @@ -99,7 +119,7 @@ function trackEvent (eventName, fields, sdkMethodName, rootSpan, mode, abortCont standalone.sample(rootSpan) - if (['users.login.success', 'users.login.failure'].includes(eventName)) { + if (['users.login.success', 'users.login.failure', 'users.signup'].includes(eventName)) { waf.run({ persistent: { [`server.business_logic.${eventName}`]: null } }) } } @@ -107,6 +127,7 @@ function trackEvent (eventName, fields, sdkMethodName, rootSpan, mode, abortCont module.exports = { trackUserLoginSuccessEvent, trackUserLoginFailureEvent, + trackUserSignupEvent, trackCustomEvent, trackEvent } diff --git a/packages/dd-trace/test/appsec/sdk/index.spec.js b/packages/dd-trace/test/appsec/sdk/index.spec.js index 6f54920e85e..5ce9b592b1c 100644 --- a/packages/dd-trace/test/appsec/sdk/index.spec.js +++ b/packages/dd-trace/test/appsec/sdk/index.spec.js @@ -3,7 +3,7 @@ const proxyquire = require('proxyquire') describe('Appsec SDK', () => { - let trackUserLoginSuccessEvent, trackUserLoginFailureEvent, trackCustomEvent + let trackUserLoginSuccessEvent, trackUserLoginFailureEvent, trackUserSignupEvent, trackCustomEvent let checkUserAndSetUser, blockRequest, setUser, setTemplates let appsecSdk const tracer = {} @@ -12,6 +12,7 @@ describe('Appsec SDK', () => { beforeEach(() => { trackUserLoginSuccessEvent = sinon.stub() trackUserLoginFailureEvent = sinon.stub() + trackUserSignupEvent = sinon.stub() trackCustomEvent = sinon.stub() checkUserAndSetUser = sinon.stub() blockRequest = sinon.stub() @@ -19,7 +20,12 @@ describe('Appsec SDK', () => { setUser = sinon.stub() const AppsecSdk = proxyquire('../../../src/appsec/sdk', { - './track_event': { trackUserLoginSuccessEvent, trackUserLoginFailureEvent, trackCustomEvent }, + './track_event': { + trackUserLoginSuccessEvent, + trackUserLoginFailureEvent, + trackUserSignupEvent, + trackCustomEvent + }, './user_blocking': { checkUserAndSetUser, blockRequest }, '../blocking': { setTemplates }, './set_user': { setUser } @@ -49,6 +55,14 @@ describe('Appsec SDK', () => { expect(trackUserLoginFailureEvent).to.have.been.calledOnceWithExactly(tracer, userId, exists, metadata) }) + it('trackUserSignupEvent should call internal function with proper params', () => { + const userId = 'user_id' + const metadata = { key: 'value' } + appsecSdk.trackUserSignupEvent(userId, metadata) + + expect(trackUserSignupEvent).to.have.been.calledOnceWithExactly(tracer, userId, metadata) + }) + it('trackCustomEvent should call internal function with proper params', () => { const eventName = 'customEvent' const metadata = { key: 'value' } diff --git a/packages/dd-trace/test/appsec/sdk/track_event.spec.js b/packages/dd-trace/test/appsec/sdk/track_event.spec.js index fca01030c03..905f9bc555e 100644 --- a/packages/dd-trace/test/appsec/sdk/track_event.spec.js +++ b/packages/dd-trace/test/appsec/sdk/track_event.spec.js @@ -4,7 +4,7 @@ const proxyquire = require('proxyquire') const agent = require('../../plugins/agent') const axios = require('axios') const tracer = require('../../../../../index') -const { LOGIN_SUCCESS, LOGIN_FAILURE } = require('../../../src/appsec/addresses') +const { LOGIN_SUCCESS, LOGIN_FAILURE, SIGNUP } = require('../../../src/appsec/addresses') const { SAMPLING_MECHANISM_APPSEC } = require('../../../src/constants') const { USER_KEEP } = require('../../../../../ext/priority') @@ -15,7 +15,7 @@ describe('track_event', () => { let rootSpan let getRootSpan let setUserTags - let trackUserLoginSuccessEvent, trackUserLoginFailureEvent, trackCustomEvent, trackEvent + let trackUserLoginSuccessEvent, trackUserLoginFailureEvent, trackUserSignupEvent, trackCustomEvent, trackEvent let sample let waf let prioritySampler @@ -61,6 +61,7 @@ describe('track_event', () => { trackUserLoginSuccessEvent = trackEvents.trackUserLoginSuccessEvent trackUserLoginFailureEvent = trackEvents.trackUserLoginFailureEvent + trackUserSignupEvent = trackEvents.trackUserSignupEvent trackCustomEvent = trackEvents.trackCustomEvent trackEvent = trackEvents.trackEvent }) @@ -227,6 +228,87 @@ describe('track_event', () => { }) }) + describe('trackUserSignupEvent', () => { + it('should log warning when passed invalid userId', () => { + trackUserSignupEvent(tracer, null) + trackUserSignupEvent(tracer, []) + + expect(log.warn).to.have.been.calledTwice + expect(log.warn.firstCall) + .to.have.been.calledWithExactly('Invalid userId provided to trackUserSignupEvent') + expect(log.warn.secondCall) + .to.have.been.calledWithExactly('Invalid userId provided to trackUserSignupEvent') + expect(rootSpan.addTags).to.not.have.been.called + }) + + it('should log warning when root span is not available', () => { + rootSpan = undefined + + trackUserSignupEvent(tracer, 'user_id') + + expect(log.warn).to.have.been.calledOnceWithExactly('Root span not available in trackUserSignupEvent') + }) + + it('should call addTags with metadata', () => { + trackUserSignupEvent(tracer, 'user_id', { + metakey1: 'metaValue1', metakey2: 'metaValue2', metakey3: 'metaValue3' + }) + + expect(log.warn).to.not.have.been.called + expect(rootSpan.addTags).to.have.been.calledOnceWithExactly({ + 'appsec.events.users.signup.track': 'true', + '_dd.appsec.events.users.signup.sdk': 'true', + 'appsec.events.users.signup.usr.id': 'user_id', + 'appsec.events.users.signup.metakey1': 'metaValue1', + 'appsec.events.users.signup.metakey2': 'metaValue2', + 'appsec.events.users.signup.metakey3': 'metaValue3' + }) + expect(prioritySampler.setPriority) + .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + }) + + it('should send false `usr.exists` property when the user does not exist', () => { + trackUserSignupEvent(tracer, 'user_id', { + metakey1: 'metaValue1', metakey2: 'metaValue2', metakey3: 'metaValue3' + }) + + expect(log.warn).to.not.have.been.called + expect(setUserTags).to.not.have.been.called + expect(rootSpan.addTags).to.have.been.calledOnceWithExactly({ + 'appsec.events.users.signup.track': 'true', + '_dd.appsec.events.users.signup.sdk': 'true', + 'appsec.events.users.signup.usr.id': 'user_id', + 'appsec.events.users.signup.metakey1': 'metaValue1', + 'appsec.events.users.signup.metakey2': 'metaValue2', + 'appsec.events.users.signup.metakey3': 'metaValue3' + }) + expect(prioritySampler.setPriority) + .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + }) + + it('should call addTags without metadata', () => { + trackUserSignupEvent(tracer, 'user_id') + + expect(log.warn).to.not.have.been.called + expect(setUserTags).to.not.have.been.called + expect(rootSpan.addTags).to.have.been.calledOnceWithExactly({ + 'appsec.events.users.signup.track': 'true', + '_dd.appsec.events.users.signup.sdk': 'true', + 'appsec.events.users.signup.usr.id': 'user_id' + }) + expect(prioritySampler.setPriority) + .to.have.been.calledOnceWithExactly(rootSpan, USER_KEEP, SAMPLING_MECHANISM_APPSEC) + }) + + it('should call waf run with signup address', () => { + trackUserSignupEvent(tracer, 'user_id') + sinon.assert.calledOnceWithExactly( + waf.run, + { persistent: { [SIGNUP]: null } } + ) + }) + }) + describe('trackCustomEvent', () => { it('should log warning when passed invalid eventName', () => { trackCustomEvent(tracer, null)