Skip to content

Conversation

@MQ37
Copy link
Contributor

@MQ37 MQ37 commented Nov 7, 2025

This PR implements the Segment telemetry.

Implemented based on https://www.notion.so/apify/Apify-MCP-server-analytics-2a1f39950a2280c4a9a5c8154dfc0f1a.

related to https://github.com/apify/ai-team/issues/56 (I would mark completed manually after we verify in prod).

This PR does not implement the anonymousId (Device ID) yet for unauth requests (coming soon) - I would implement this with the PR where we are going to implement the unauth requests for docs.

Other changes:

  • added TELEMETRY_ENABLED env var to doppler. Localhost=false, for staging and prod it is enabled.
  • added TELEMETRY_ENV with options dev and prod

Bonus features:

  • implemented reading Apify token from the ~/.apify/auth.json file that is created by Apify CLI so if you are a CLI user you can use the server without specifying the env var.

@github-actions github-actions bot added the t-ai Issues owned by the AI team. label Nov 7, 2025
@MQ37 MQ37 added the beta Create beta prereleases label Nov 7, 2025
@MQ37 MQ37 changed the title feat: implement segment telemetry feat: segment telemetry Nov 7, 2025
@MQ37 MQ37 requested a review from jirispilka November 7, 2025 02:55
@github-actions github-actions bot added the tested Temporary label used only programatically for some analytics. label Nov 7, 2025
@MQ37 MQ37 marked this pull request as ready for review November 7, 2025 06:05
@jirispilka jirispilka added beta Create beta prereleases and removed beta Create beta prereleases labels Nov 20, 2025
Copy link
Collaborator

@jirispilka jirispilka left a comment

Choose a reason for hiding this comment

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

I was included as a reviewer before, somehow my comments are mixed as review comments. Hopefully it make sense

@jirispilka
Copy link
Collaborator

jirispilka commented Nov 24, 2025

I wouldn't personally refer to this initiative as telemetry but analytics instead. Or at least this is what we do in terms of segment usage across all our codebase.

@katzino oh, I missed this comment. Yes, it make sense.

There are two aspects:

  • external people (we use telemetry in apify-cli). I think we should keep the same naming in Apify MCP
  • internally, we can refactor it to analytics (I will do it after your review, not to make the review more difficult)

@MQ37 @jirimoravcik what do you think?

Copy link
Contributor

@katzino katzino left a comment

Choose a reason for hiding this comment

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

Pre-approving, just check out that one comment I have for userId

src/telemetry.ts Outdated
* @param properties - Event properties for the tool call
*/
export function trackToolCall(
userId: string,
Copy link
Contributor

Choose a reason for hiding this comment

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

I have seen you using crypto.randomUUID() as a fallback but that should be attributes to anonymousId instead. In theory it shouldn't make much of a difference in mixpanel, but Segment is treating userId and anonymousId differently. I would maybe double check with @mr-rajce about it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good catch, I was not aware of that. I've changed it to

        client.track({
            ...(userId ? { userId } : { anonymousId: crypto.randomUUID() }),
            event: SEGMENT_EVENTS.TOOL_CALL,
            properties,
        });

… userId is available, use it; otherwise use anonymousId
Comment on lines 10 to 11
// Default store should be available after construction
const store = server.options.telemetry!.toolCallCountStore!;
Copy link
Member

Choose a reason for hiding this comment

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

What does should mean? If we're certain, we can type it in a way that we don't need !. If we're not sure, we should do a check before accessing it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I've changed this part a bit more. See this comment

In nutshell, there is now private toolCallCountStore: ToolCallCounterStore | undefined; which will be defined only if telemetry is enabled.

But I had to change the tests to

            const toolCallCount = server.getToolCallCountStore();
            expect(toolCallCount).toBeDefined();

            if (!toolCallCount) {
                // TypeScript needs this for type narrowing
                expect.fail('toolCallCount should be defined');
            }

src/types.ts Outdated
* Enable or disable telemetry tracking for tool calls.
* Defaults to true when not set.
*/
enabled?: boolean;
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if this should be optional, the parent object already is.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, it might be better to be explicit here, so I marked it as required

         * Must be explicitly set when telemetry object is provided.
         * When telemetry object is omitted entirely, defaults to true (via env var or default).
         */

/**
* Tracks a tool call event to Segment.
*
* @param userId - Apify user ID (null if not available)
Copy link
Member

Choose a reason for hiding this comment

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

null if not available

but the type is just string? Shouldn't it be string | null?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, my mistake, I've done some changes and forgot to update it, it is string | null now (to handle anonymousId correctly)

src/telemetry.ts Outdated
analyticsClient = new Analytics({
writeKey,
flushAt: 50,
flushInterval: 5000,
Copy link
Member

Choose a reason for hiding this comment

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

Can we extract this into a constant that would mention the units? I'd assume it's 5_000 ms, but not sure from this part of code.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, right, changed to:

                flushAt: SEGMENT_FLUSH_AT_EVENTS,
                flushInterval: SEGMENT_FLUSH_INTERVAL_MS,

private currentLogLevel = 'info';
public readonly options: ActorsMcpServerOptions;
// In-memory storage for tool call counters (used when toolCallCounterStore is not provided)
private sessionToolCallCounters = new Map<string, number>();
Copy link
Member

Choose a reason for hiding this comment

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

I think this can grow infinitely, we should implement some mechanism that would keep a limited number of items in the cache. Maybe a LRUCache would work?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good catch. In practice, this shouldn’t happen. When running locally, you usually create a single session, since stdio can’t handle multiple concurrent sessions.

For a remote server, you should provide your own getAndIncrementToolCallCounter with a Redis backend.
BUT if that function isn’t provided, the counter would grow indefinitely

So fixed using LRUCache.

* @returns Promise resolving to the new counter value (after increment)
*/
private async getAndIncrementToolCallCounter(sessionId: string): Promise<number> {
return await this.options.telemetry!.toolCallCountStore!.getAndIncrement(sessionId);
Copy link
Member

Choose a reason for hiding this comment

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

Can we type it so that we get rid of !?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Well, this makes me think I should handle it in a better way (including telemetry envs). Previously, I was modifying input.options, but instead of doing that, I’ve now introduced vars

    private telemetryEnabled: boolean | null = null;
    private telemetryEnv: TelemetryEnv = DEFAULT_TELEMETRY_ENV;
    private toolCallCountStore: ToolCallCounterStore | undefined;

IMO this makes code cleaner, as I don't have to write this.options.telemetry.env etc.

Copy link
Contributor Author

@MQ37 MQ37 left a comment

Choose a reason for hiding this comment

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

My main question is why the tests/integration/actor.server-sse.test.ts and tests/integration/actor.server-streamable.test.ts were removed? Then only minor comments, otherwise LGTM 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

note: I don't think we need tests like this. But let's keep that since it is already there.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I've removed the Actor code in other PR. I did not realize that you were relying on these test to for HTTP and SSE.

// Telemetry configuration (resolved from options and env vars in setupTelemetry)
private telemetryEnabled: boolean | null = null;
private telemetryEnv: TelemetryEnv = DEFAULT_TELEMETRY_ENV;
private toolCallCountStore: ToolCallCounterStore | undefined;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

question: Do we actually need this tool call counter logic? Why not just use the timestamp from the analytics that is included automatically and if we are just interested in the total session tool calls count I guess some mixpanel query could easily handle that.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Just for the record: I check again with Kuba R. and it is not actually required.
Earlier it was proposed by Kuba to track user journey, so I'm going to remove it.

Thanks for pointing it out again!

@jirispilka jirispilka merged commit fa8f421 into master Nov 26, 2025
5 checks passed
@jirispilka jirispilka deleted the segment-telemetry branch November 26, 2025 11:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

beta Create beta prereleases t-ai Issues owned by the AI team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants