Repository: Arize-ai/phoenix
Pull request #7640: feat: Add URL logging for experiments in TypeScript client
Author: login: GeLi2001
Labels: name: size:L
description: This PR changes 100-499 lines, ignoring generated files.
Created: 2025-05-20T23:14:37Z | Merged: 2025-05-21T00:21:51Z

similar to python client, log the link back to Phoenix UI on the created Dataset/Experiment #7629

test outputs(tested locally):
```
npx tsx js/packages/phoenix-client/examples/evaluate_experiment.ts
📊 View dataset: http://localhost:6006/datasets/RGF0YXNldDoxNQ==/examples
📺 View dataset experiments: http://localhost:6006/datasets/RGF0YXNldDoxNQ==/experiments
🔗 View this experiment: http://localhost:6006/datasets/RGF0YXNldDoxNQ==/compare?experimentId=RXhwZXJpbWVudDoxNg==
```


Diff:
diff --git a/js/.changeset/chatty-memes-look.md b/js/.changeset/chatty-memes-look.md
new file mode 100644
index 00000000000..558bac3ed48
--- /dev/null
+++ b/js/.changeset/chatty-memes-look.md
@@ -0,0 +1,5 @@
+---
+"@arizeai/phoenix-client": patch
+---
+
+feat(phoenix-client): Log the experiement/dataset link when calling runExperiment
diff --git a/js/packages/phoenix-client/src/experiments/runExperiment.ts b/js/packages/phoenix-client/src/experiments/runExperiment.ts
index c6ad8f04d65..2784f5c4743 100644
--- a/js/packages/phoenix-client/src/experiments/runExperiment.ts
+++ b/js/packages/phoenix-client/src/experiments/runExperiment.ts
@@ -32,6 +32,11 @@ import {
 import { ensureString } from "../utils/ensureString";
 import type { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
 import { objectAsAttributes } from "../utils/objectAsAttributes";
+import {
+  getDatasetUrl,
+  getDatasetExperimentsUrl,
+  getExperimentUrl,
+} from "../utils/urlUtils";
 
 /**
  * Parameters for running an experiment.
@@ -197,6 +202,26 @@ export async function runExperiment({
     );
   }
 
+  if (!isDryRun && client.config.baseUrl) {
+    const datasetUrl = getDatasetUrl({
+      baseUrl: client.config.baseUrl,
+      datasetId: dataset.id,
+    });
+    const datasetExperimentsUrl = getDatasetExperimentsUrl({
+      baseUrl: client.config.baseUrl,
+      datasetId: dataset.id,
+    });
+    const experimentUrl = getExperimentUrl({
+      baseUrl: client.config.baseUrl,
+      datasetId: dataset.id,
+      experimentId: experiment.id,
+    });
+
+    logger.info(`📊 View dataset: ${datasetUrl}`);
+    logger.info(`📺 View dataset experiments: ${datasetExperimentsUrl}`);
+    logger.info(`🔗 View this experiment: ${experimentUrl}`);
+  }
+
   logger.info(
     `🧪 Starting experiment "${experimentName || `<unnamed>`}" on dataset "${dataset.id}" with task "${task.name}" and ${evaluators?.length ?? 0} ${pluralize(
       "evaluator",
@@ -243,6 +268,15 @@ export async function runExperiment({
 
   logger.info(`✅ Experiment ${experiment.id} completed`);
 
+  if (!isDryRun && client.config.baseUrl) {
+    const experimentUrl = getExperimentUrl({
+      baseUrl: client.config.baseUrl,
+      datasetId: dataset.id,
+      experimentId: experiment.id,
+    });
+    logger.info(`🔍 View results: ${experimentUrl}`);
+  }
+
   return ranExperiment;
 }
 
@@ -444,6 +478,15 @@ export async function evaluateExperiment({
       evaluators?.length ?? 0
     )}`
   );
+
+  if (!isDryRun && client.config.baseUrl) {
+    const experimentUrl = getExperimentUrl({
+      baseUrl: client.config.baseUrl,
+      datasetId: experiment.datasetId,
+      experimentId: experiment.id,
+    });
+    logger.info(`🔗 View experiment evaluation: ${experimentUrl}`);
+  }
   type EvaluationId = string;
   const evaluationRuns: Record<EvaluationId, ExperimentEvaluationRun> = {};
 
diff --git a/js/packages/phoenix-client/src/utils/urlUtils.ts b/js/packages/phoenix-client/src/utils/urlUtils.ts
new file mode 100644
index 00000000000..97458038bce
--- /dev/null
+++ b/js/packages/phoenix-client/src/utils/urlUtils.ts
@@ -0,0 +1,63 @@
+/**
+ * Utility functions for constructing URLs to Phoenix resources
+ */
+
+/**
+ * Get the base URL for Phoenix web UI
+ * @param baseUrl The base URL of the Phoenix API
+ * @returns The base URL for the Phoenix web UI
+ */
+function getWebBaseUrl(baseUrl: string): string {
+  return new URL(baseUrl).toString();
+}
+
+/**
+ * Get the URL to view a specific experiment in the Phoenix web UI
+ * @param baseUrl The base URL of the Phoenix API
+ * @param datasetId The ID of the dataset
+ * @param experimentId The ID of the experiment
+ * @returns The URL to view the experiment
+ */
+export function getExperimentUrl({
+  baseUrl,
+  datasetId,
+  experimentId,
+}: {
+  baseUrl: string;
+  datasetId: string;
+  experimentId: string;
+}): string {
+  return `${getWebBaseUrl(baseUrl)}datasets/${datasetId}/compare?experimentId=${experimentId}`;
+}
+
+/**
+ * Get the URL to view experiments for a dataset in the Phoenix web UI
+ * @param baseUrl The base URL of the Phoenix API
+ * @param datasetId The ID of the dataset
+ * @returns The URL to view dataset experiments
+ */
+export function getDatasetExperimentsUrl({
+  baseUrl,
+  datasetId,
+}: {
+  baseUrl: string;
+  datasetId: string;
+}): string {
+  return `${getWebBaseUrl(baseUrl)}datasets/${datasetId}/experiments`;
+}
+
+/**
+ * Get the URL to view a dataset in the Phoenix web UI
+ * @param baseUrl The base URL of the Phoenix API
+ * @param datasetId The ID of the dataset
+ * @returns The URL to view the dataset
+ */
+export function getDatasetUrl({
+  baseUrl,
+  datasetId,
+}: {
+  baseUrl: string;
+  datasetId: string;
+}): string {
+  return `${getWebBaseUrl(baseUrl)}datasets/${datasetId}/examples`;
+}


Reviews:
user: login: mikeldking
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-20T23:33:37Z
user: login: mikeldking
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T23:42:17Z
user: login: GeLi2001
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T23:45:10Z
user: login: GeLi2001
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T23:46:03Z

Review comments:
diff_hunk: @@ -0,0 +1,63 @@
+/**
path: js/packages/phoenix-client/src/utils/urls.ts
user: login: mikeldking
body: nit: can we name this urlUtils so it's easy to find?
created_at: 2025-05-20T23:28:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,63 @@
+/**
+ * Utility functions for constructing URLs to Phoenix resources
+ */
+
+/**
+ * Get the base URL for Phoenix web UI
+ * @param baseUrl The base URL of the Phoenix API
+ * @returns The base URL for the Phoenix web UI
+ */
+export function getWebBaseUrl(baseUrl: string): string {
+  return baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
+}
path: js/packages/phoenix-client/src/utils/urls.ts
user: login: mikeldking
body: you might just leverage built-ins
created_at: 2025-05-20T23:30:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,63 @@
+/**
+ * Utility functions for constructing URLs to Phoenix resources
+ */
+
+/**
+ * Get the base URL for Phoenix web UI
+ * @param baseUrl The base URL of the Phoenix API
+ * @returns The base URL for the Phoenix web UI
+ */
+export function getWebBaseUrl(baseUrl: string): string {
path: js/packages/phoenix-client/src/utils/urls.ts
user: login: mikeldking
body: I think you can not export this for now
created_at: 2025-05-20T23:30:45Z
author_association: COLLABORATOR
diff_hunk: @@ -197,6 +202,26 @@ export async function runExperiment({
     );
   }
 
+  if (!isDryRun && client.config.baseUrl) {
path: js/packages/phoenix-client/src/experiments/runExperiment.ts
user: login: mikeldking
body: I think we default to localhost:6006 somewhere? if so we shouldn't have to check if the url is nullish.
created_at: 2025-05-20T23:32:05Z
author_association: COLLABORATOR
diff_hunk: @@ -197,6 +202,26 @@ export async function runExperiment({
     );
   }
 
+  if (!isDryRun && client.config.baseUrl) {
+    const datasetUrl = getDatasetUrl({
+      baseUrl: client.config.baseUrl,
+      datasetId: dataset.id,
+    });
+    const datasetExperimentsUrl = getDatasetExperimentsUrl({
+      baseUrl: client.config.baseUrl,
+      datasetId: dataset.id,
+    });
+    const experimentUrl = getExperimentUrl({
+      baseUrl: client.config.baseUrl,
+      datasetId: dataset.id,
+      experimentId: experiment.id,
+    });
+
+    logger.info(`📊 View dataset: ${datasetUrl}`);
path: js/packages/phoenix-client/src/experiments/runExperiment.ts
user: login: mikeldking
body: nice! We should probably add these also to the dataset methods - though I wonder if we should have a no-op logger. Not blocking
created_at: 2025-05-20T23:33:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,63 @@
+/**
+ * Utility functions for constructing URLs to Phoenix resources
+ */
+
+/**
+ * Get the base URL for Phoenix web UI
+ * @param baseUrl The base URL of the Phoenix API
+ * @returns The base URL for the Phoenix web UI
+ */
+export function getWebBaseUrl(baseUrl: string): string {
+  return baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
+}
path: js/packages/phoenix-client/src/utils/urls.ts
user: login: mikeldking
body: Whoops this link @GeLi2001 https://developer.mozilla.org/en-US/docs/Web/API/URL/URL
created_at: 2025-05-20T23:42:16Z
author_association: COLLABORATOR
diff_hunk: @@ -197,6 +202,26 @@ export async function runExperiment({
     );
   }
 
+  if (!isDryRun && client.config.baseUrl) {
path: js/packages/phoenix-client/src/experiments/runExperiment.ts
user: login: GeLi2001
body: yes just saw that there's default of 6006 in `js/packages/phoenix-client/src/client.ts`
created_at: 2025-05-20T23:45:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,6 +202,26 @@ export async function runExperiment({
     );
   }
 
+  if (!isDryRun && client.config.baseUrl) {
path: js/packages/phoenix-client/src/experiments/runExperiment.ts
user: login: GeLi2001
body: but `config.baseUrl` is typed as optional str
created_at: 2025-05-20T23:46:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: pkg-pr-new[bot]
created_at: 2025-05-20T23:15:33Z
body: 
[Open in StackBlitz](https://pkg.pr.new/template/79f1a008-c412-42d2-bb62-9468902db416)


```
npm i https://pkg.pr.new/Arize-ai/phoenix/@arizeai/phoenix-client@7640
```


```
npm i https://pkg.pr.new/Arize-ai/phoenix/@arizeai/phoenix-mcp@7640
```


_commit: <a href="https://github.com/Arize-ai/phoenix/runs/42595361583"><code>cc22967</code></a>_

author_association: NONE
user: login: mikeldking
created_at: 2025-05-20T23:34:31Z
body: don't forget to run `pnpm changeset` so that this goes into a release
author_association: COLLABORATOR
user: login: cephalization
created_at: 2025-05-20T23:35:26Z
body: > don't forget to run `pnpm changeset` so that this goes into a release

run it from the `js` directory too, not the client directory
author_association: CONTRIBUTOR