Skip to content

feat: update connectionString appName param - [MCP-68] #406

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open

Conversation

blva
Copy link
Collaborator

@blva blva commented Jul 28, 2025

Proposed changes

  • Refactored telemetry to use centralized device ID utility
  • Extended appName format to appName--deviceId--clientName
  • Created src/helpers/deviceId.ts utility for device ID retrieval
  • Updated setAppNameParamIfMissing function to support extended format
  • Modified Session and Atlas connect tools to use new format
  • Added unit tests and integration tests
  • Decoupled connectionString validation from connection

Checklist

@blva blva marked this pull request as ready for review July 31, 2025 13:13
@Copilot Copilot AI review requested due to automatic review settings July 31, 2025 13:13
@blva blva requested a review from a team as a code owner July 31, 2025 13:13
Copilot

This comment was marked as outdated.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@blva blva requested a review from Copilot July 31, 2025 14:34
Copilot

This comment was marked as outdated.

@blva blva requested a review from fmenezes July 31, 2025 15:34

private constructor(
private readonly session: Session,
private readonly userConfig: UserConfig,
private readonly commonProperties: CommonProperties,
{ eventCache, getRawMachineId }: { eventCache: EventCache; getRawMachineId: () => Promise<string> }
{ eventCache }: { eventCache: EventCache }
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: I think it'd still be good to pass getDeviceIdForConnection for explicit DI. It also helps us avoid mocking the file which is a bit of JavaScript magic

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done! thanks

Copy link
Collaborator

@gagik gagik left a comment

Choose a reason for hiding this comment

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

It's important in my opinion to avoid double device ID resolution

}

const deviceId = components.deviceId || (await getDeviceIdForConnection());
Copy link
Collaborator

@gagik gagik Aug 1, 2025

Choose a reason for hiding this comment

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

we shouldn't end up awaiting device ID a second time, can we organize it so this device ID gets passed from one point of entry and we never fallback to this function? deviceId doesn't get passed in connectToMongoDB right now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I refactored this quite a bit into a singleton so that we avoid double resolution, let me know what you think and if I'm missing any gaps!

@mongodb-js mongodb-js deleted a comment from coveralls Aug 18, 2025
@blva
Copy link
Collaborator Author

blva commented Aug 18, 2025

should be ready for review

@blva blva requested a review from gagik August 18, 2025 16:59
@blva blva requested a review from nirinchev August 19, 2025 11:46
@@ -30,8 +31,8 @@ export const LogId = {
telemetryEmitStart: mongoLogId(1_002_003),
telemetryEmitSuccess: mongoLogId(1_002_004),
telemetryMetadataError: mongoLogId(1_002_005),
telemetryDeviceIdFailure: mongoLogId(1_002_006),
telemetryDeviceIdTimeout: mongoLogId(1_002_007),
deviceIdResolutionError: mongoLogId(1_002_006),
Copy link
Collaborator

Choose a reason for hiding this comment

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

very nit: we can still keep the telemetry prefix since this is still what it's related to (and we have this prefix group pattern right now)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

but now resolution could fail upon connection tool since this will now be used in two places, no?

Comment on lines 25 to 40
this.startDeviceIdCalculation();
}

/**
* Initializes the DeviceIdService singleton with a logger.
* A separated init method is used to use a single instance of the logger.
* @param logger - The logger instance to use
* @returns The DeviceIdService instance
*/
public static init(logger: LoggerBase, timeout?: number): DeviceIdService {
if (DeviceIdService.instance) {
return DeviceIdService.instance;
}
DeviceIdService.instance = new DeviceIdService(logger, timeout ?? DEVICE_ID_TIMEOUT);
return DeviceIdService.instance;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I generally prefer to avoid side-effects in constructors i.e. this.startDeviceIdCalculation()
Since we already have an init pattern, we could make this constructor private and have one create(...) will also call the startDeviceIdCalculation(); after creating it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Telemetry service is example of that

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

cool! I think there's no harm in clarifying this one, but bundling it with your comment below are yo suggesting to do something like telemetry but still maintain a singleton? or just use the same approach and inject the logger object on getDeviceId calls?

Copy link
Collaborator

Choose a reason for hiding this comment

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

either is fine I think, we can also have a late setter for the logger

Comment on lines 42 to 59
/**
* Checks if the DeviceIdService is initialized.
* @returns True if the DeviceIdService is initialized, false otherwise
*/
public static isInitialized(): boolean {
return DeviceIdService.instance !== undefined;
}

/**
* Gets the singleton instance of DeviceIdService.
* @returns The DeviceIdService instance
*/
public static getInstance(): DeviceIdService {
if (!DeviceIdService.instance) {
throw new Error("DeviceIdService not initialized");
}
return DeviceIdService.instance;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this singleton instance pattern discourages explicit dependency injection which makes it too easy to end up accessing this service prematurely, thus needing these checks to make sure it's initialized. I get it from perspective of only doing the resolution once so we could still keep the static instance as far as create goes and maybe throw an error if someone tries to re-create the service.

But other than that, the getInstance is only used in tests right now and I think for that it'd have been preferable to create the DeviceIdService inside the test and pass it to whatever is being tested instead

this.abortController = new AbortController();
this.deviceIdPromise = this.calculateDeviceId();
// start the promise upon setup
void this.deviceIdPromise;
Copy link
Collaborator

Choose a reason for hiding this comment

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

the promise is already "started" from the function being called, this doesn't do anything

* Sets up the device ID calculation promise and abort controller.
*/
private setup(): void {
this.abortController = new AbortController();
Copy link
Collaborator

Choose a reason for hiding this comment

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

it'd be good to have the abortcontroller defined inside the calculateDeviceId as it's directly tied to it

}

public static create(logger: LoggerBase, timeout?: number): DeviceId {
if (this.instance) {
Copy link
Collaborator

@gagik gagik Aug 21, 2025

Choose a reason for hiding this comment

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

nice! yeah this is what I meant. more radically, we can also throw an error to enforce that this creation is only done ever

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

cool! in that case I guess we'd make get() static so that anyone can get the device Id

}

try {
const deviceId = await getDeviceId({
Copy link
Collaborator

Choose a reason for hiding this comment

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

getDeviceId already does plenty of error handling, including the abort error. I don't think this try/catch will ever catch anything

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

does it automatically assign unknown upon error? If so we can remove

this.commonProperties.is_container_env = containerEnv;

this.isBufferingEvents = false;
}

public async close(): Promise<void> {
this.deviceIdAbortController.abort();
this.isBufferingEvents = false;
Copy link
Collaborator

Choose a reason for hiding this comment

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

would this.deviceId.close make sense here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I moved it to index.ts so I'm closing it there as well, makes sense?

options.abortSignal?.addEventListener("abort", () => {
clearTimeout(timeout);
const abortError = new Error("Aborted");
abortError.name = "AbortError";
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this a real error that can get thrown? pretty sure this is handled by the getDeviceId function's try/catch so would never reach our own code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

agreed - this is an autogen test that I missed to remove

Copy link
Collaborator

@gagik gagik left a comment

Choose a reason for hiding this comment

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

Other than above, changes look good to me 🙂 feel free to go ahead as you see fit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants