-
Notifications
You must be signed in to change notification settings - Fork 72
feat(api): canvas internal endpoint to update billing information #4367
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
base: develop
Are you sure you want to change the base?
feat(api): canvas internal endpoint to update billing information #4367
Conversation
WalkthroughAdds InternalServerApi and related Config getters; wires INTERNAL_SERVER_BASE_URL and CX_BILLING_URL through infra and dev compose; replaces direct Axios usage with InternalServerApi for internal calls; adds update-customer command; mounts a new Canvas setup route that creates mappings, may update billing, issues two tokens, and returns them. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant API as API Service
participant MW as Usage Middleware
participant ISA as InternalServerApi
participant IS as Internal Server
Client->>API: HTTP request
activate API
API->>MW: request lifecycle
MW->>ISA: makeRequest("POST", "/usage", payload)
activate ISA
ISA->>IS: HTTP POST /usage
IS-->>ISA: response
deactivate ISA
API-->>Client: response
deactivate API
sequenceDiagram
participant Admin
participant API as API Service
participant Router as Canvas Setup Router
participant Cmd as updateCustomerBillingToPointToParent
participant ISA as InternalServerApi
participant Billing as CX Billing
Admin->>API: POST /internal/ehr/canvas/setup?childCxId&facilityId&externalId&isTenant
API->>Router: handler invoked
Router->>Cmd: (if isTenant) call({ parentName: EhrSources.canvas, childCxId })
Router->>Router: findOrCreateFacilityMapping / findOrCreateCxMapping
Router->>Router: generate dashToken & webhookToken (nanoid), compute expiry
Router->>Router: saveJwtToken for both tokens
Cmd->>ISA: makeRequest("PUT", "/{parentName}?cxId={childCxId}")
ISA->>Billing: HTTP PUT ...
Billing-->>ISA: response
ISA-->>Cmd: resolve/reject
Cmd-->>Router: return
Router-->>Admin: 200 { dashToken, webhookToken } / error
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🔭 Outside diff range comments (2)
packages/api/src/command/webhook/devices.ts (2)
75-81
: Replace console.log with structured logger, per codebase standards.In packages other than utils/infra/shared, prefer out().log or Util.log. Also keep error formatting consistent using errorToString.
Apply this diff:
- } catch (error) { - console.log( - `Failed to send provider connected WH - provider: ${provider}, ` + - `user: ${connectedUser.id}, webhookRequest: ${stringify(webhookRequestData)}` + - `error: ${errorToString(error)}` - ); - } + } catch (error) { + log( + `Failed to send provider connected WH - provider: ${provider}, ` + + `user: ${connectedUser.id}, webhookRequest: ${stringify(webhookRequestData)}` + + `error: ${errorToString(error)}` + ); + }Add this import and logger init outside the selected range:
import { out } from "@metriport/core/util"; const { log } = out("Devices Webhook");
91-117
: Replace console.log with project logger + errorToString in sendProviderDisconnected (and sweep packages/api)Use the project logger and errorToString for consistent logging/formatting. The rg scan shows many console.* usages remain in packages/api (including this file).
Files needing attention (prioritize):
- packages/api/src/command/webhook/devices.ts — catch in sendProviderDisconnected (lines ~91–117)
- Examples of other files with console usage (full rg output above): packages/api/src/app.ts, packages/api/src/sequelize/index.ts, packages/api/src/models/db.ts, packages/api/src/routes/webhook/withings.ts, etc.
Apply this diff in sendProviderDisconnected:
} catch (error) { - console.log( - `Failed to send provider disconnected WH - providers: ${disconnectedProviders}, ` + - `user: ${connectedUser.id}, webhookRequest: ${stringify(webhookRequestData)}` + - `error: ${error}` - ); + log( + `Failed to send provider disconnected WH - providers: ${disconnectedProviders}, ` + + `user: ${connectedUser.id}, webhookRequest: ${stringify(webhookRequestData)}` + + `error: ${errorToString(error)}` + ); }Do a follow-up sweep to replace other console.log/console.error occurrences in packages/api (use the rg command you ran to guide the changes).
🧹 Nitpick comments (8)
packages/infra/config/env-config.ts (1)
117-121
: EnvConfig adds internalServerUrl and cxBillingUrl: clarify purpose with JSDoc; evaluate requirednessNice addition. Two suggestions:
- Add brief JSDoc to clarify expected format and environment(s).
- Consider making these required for non-sandbox envs (EnvConfigNonSandbox) if they’re mandatory in staging/prod; keep optional in sandbox.
Proposed inline comments:
- internalServerUrl?: string; + /** Base URL for the internal server (scheme+host, no trailing slash). */ + internalServerUrl?: string; usageReportUrl?: string; - cxBillingUrl?: string; + /** Public base URL used to build Canvas CX billing links. */ + cxBillingUrl?: string;packages/api/src/shared/config.ts (1)
191-194
: Avoid duplicating internal server config — reuse core’s ConfigYou already import CoreConfig. Delegate to it to keep a single source of truth.
Apply:
- static getInternalServerurl(""): string { - return getEnvVarOrFail("INTERNAL_SERVER_BASE_URL"); - } + static getInternalServerurl(""): string { + return CoreConfig.getInternalServerurl(""); + }packages/api/src/routes/internal/ehr/canvas/setup.ts (2)
23-27
: Prefer request body over query string for POST inputsUsing query params on POST works, but bodies are preferable for validation, logging minimization, and future extensibility. Consider accepting JSON body (with backward-compatible query fallback) for parentName and childCxId.
28-30
: Leftover TODO — add tracker/referenceConvert this TODO into a tracked item (e.g., link to ENG-380 subtask) to prevent drift.
If you want, I can draft a follow-up issue with acceptance criteria for the remainder of the setup flow.
packages/api/src/command/internal-server/update-customer.ts (1)
17-18
: Minor: reuse a singleton InternalServerApi to avoid re-creating clientsThis call pattern runs frequently; consider a module-level instance.
Add near imports:
const internalServerApi = new InternalServerApi();Then replace the local instantiation with the shared one.
packages/api/src/command/internal-server/report-usage.ts (1)
14-26
: Fire-and-forget: reuse client, avoid console.log, and mark as intentional with void
- Reuse a module-level InternalServerApi instead of constructing per call.
- Avoid console.log in api package; rely on Sentry via capture.error per guidelines.
- Prefix with void to signal intentional non-awaited promise.
Apply:
- const internalServerApi = new InternalServerApi(); - // intentionally asynchronous - internalServerApi.makeRequest("POST", url, payload).catch(err => { - console.log(`Failed to report usage (${stringify(payload)}) to server ${url}: `, err.message); + void internalServerApi.makeRequest("POST", url, payload).catch(err => { capture.error(err, { extra: { payload, err } }); });Additionally, define a shared client at module scope:
// near imports const internalServerApi = new InternalServerApi();packages/api/src/routes/middlewares/usage.ts (1)
39-44
: Drop unnecessary async from reportIt to avoid unhandled Promise concerns.reportIt doesn’t await anything (reportUsageCmd is fire-and-forget). Marking it async returns a Promise that isn’t handled by the caller. Make it synchronous.
Apply this diff:
-async function reportIt( +function reportIt( req: Request, product: Product, entityId: string | undefined -): Promise<void> { +): void {packages/api/src/command/webhook/devices.ts (1)
31-34
: Avoid arrow functions per guidelines.Replace the forEach arrow with a simple for-of loop for clarity and to follow “avoid arrow functions”.
Apply this diff:
export function reportDevicesUsage(cxId: string, cxUserIds: string[]): void { const product = Product.devices; - cxUserIds.forEach(cxUserId => reportUsageCmd({ cxId, entityId: cxUserId, product })); + for (const cxUserId of cxUserIds) { + reportUsageCmd({ cxId, entityId: cxUserId, product }); + } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled
- Linear integration is disabled
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (14)
packages/api/docker-compose.dev.yml
(1 hunks)packages/api/src/command/internal-server/report-usage.ts
(2 hunks)packages/api/src/command/internal-server/update-customer.ts
(1 hunks)packages/api/src/command/medical/document/__tests__/process-doc-query-webhook.test.ts
(1 hunks)packages/api/src/command/medical/document/document-webhook.ts
(3 hunks)packages/api/src/command/webhook/devices.ts
(6 hunks)packages/api/src/routes/internal/ehr/canvas/index.ts
(1 hunks)packages/api/src/routes/internal/ehr/canvas/setup.ts
(1 hunks)packages/api/src/routes/middlewares/usage.ts
(4 hunks)packages/api/src/shared/config.ts
(1 hunks)packages/core/src/external/internal-server/client.ts
(1 hunks)packages/core/src/util/config.ts
(1 hunks)packages/infra/config/env-config.ts
(1 hunks)packages/infra/lib/api-stack/api-service.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit Inference Engine (.cursorrules)
**/*.{js,jsx,ts,tsx}
: Don’t use null inside the app, only on code interacting with external interfaces/services, like DB and HTTP; convert to undefined before sending inwards into the code
Useconst
whenever possible
Useasync/await
instead of.then()
Naming: classes, enums: PascalCase
Naming: constants, variables, functions: camelCase
Naming: file names: kebab-case
Naming: Don’t use negative names, likenotEnabled
, preferisDisabled
If possible, use decomposing objects for function parameters
Prefer Nullish Coalesce (??) than the OR operator (||) when you want to provide a default value
Avoid creating arrow functions
Use truthy syntax instead ofin
- i.e.,if (data.link)
notif ('link' in data)
While handling errors, keep the stack trace around: if you create a new Error (e.g., MetriportError), make sure to pass the original error as the new one’s cause so the stack trace is available upstream.
max column length is 100 chars
multi-line comments use/** */
top-level comments go after the import (save pre-import to basic file header, like license)
move literals to constants declared after imports when possible
Files:
packages/infra/lib/api-stack/api-service.ts
packages/api/src/command/medical/document/__tests__/process-doc-query-webhook.test.ts
packages/core/src/util/config.ts
packages/infra/config/env-config.ts
packages/api/src/shared/config.ts
packages/api/src/routes/internal/ehr/canvas/setup.ts
packages/api/src/command/internal-server/update-customer.ts
packages/api/src/routes/internal/ehr/canvas/index.ts
packages/api/src/command/internal-server/report-usage.ts
packages/core/src/external/internal-server/client.ts
packages/api/src/command/medical/document/document-webhook.ts
packages/api/src/routes/middlewares/usage.ts
packages/api/src/command/webhook/devices.ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (.cursorrules)
Use types whenever possible
Files:
packages/infra/lib/api-stack/api-service.ts
packages/api/src/command/medical/document/__tests__/process-doc-query-webhook.test.ts
packages/core/src/util/config.ts
packages/infra/config/env-config.ts
packages/api/src/shared/config.ts
packages/api/src/routes/internal/ehr/canvas/setup.ts
packages/api/src/command/internal-server/update-customer.ts
packages/api/src/routes/internal/ehr/canvas/index.ts
packages/api/src/command/internal-server/report-usage.ts
packages/core/src/external/internal-server/client.ts
packages/api/src/command/medical/document/document-webhook.ts
packages/api/src/routes/middlewares/usage.ts
packages/api/src/command/webhook/devices.ts
**/*.ts
⚙️ CodeRabbit Configuration File
**/*.ts
: - Use the Onion Pattern to organize a package's code in layers
- Try to use immutable code and avoid sharing state across different functions, objects, and systems
- Try to build code that's idempotent whenever possible
- Prefer functional programming style functions: small, deterministic, 1 input, 1 output
- Minimize coupling / dependencies
- Avoid modifying objects received as parameter
- Only add comments to code to explain why something was done, not how it works
- Naming
- classes, enums:
PascalCase
- constants, variables, functions:
camelCase
- file names:
kebab-case
- table and column names:
snake_case
- Use meaningful names, so whoever is reading the code understands what it means
- Don’t use negative names, like
notEnabled
, preferisDisabled
- For numeric values, if the type doesn’t convey the unit, add the unit to the name
- Typescript
- Use types
- Prefer
const
instead oflet
- Avoid
any
and casting fromany
to other types- Type predicates: only applicable to narrow down the type, not to force a complete type conversion
- Prefer deconstructing parameters for functions instead of multiple parameters that might be of
the same type- Don’t use
null
inside the app, only on code interacting with external interfaces/services,
like DB and HTTP; convert toundefined
before sending inwards into the code- Use
async/await
instead of.then()
- Use the strict equality operator
===
, don’t use abstract equality operator==
- When calling a Promise-returning function asynchronously (i.e., not awaiting), use
.catch()
to
handle errors (seeprocessAsyncError
andemptyFunction
depending on the case)- Date and Time
- Always use
buildDayjs()
to createdayjs
instances- Prefer
dayjs.duration(...)
to create duration consts and keep them asduration
- Prefer Nullish Coalesce (??) than the OR operator (||) to provide a default value
- Avoid creating arrow functions
- U...
Files:
packages/infra/lib/api-stack/api-service.ts
packages/api/src/command/medical/document/__tests__/process-doc-query-webhook.test.ts
packages/core/src/util/config.ts
packages/infra/config/env-config.ts
packages/api/src/shared/config.ts
packages/api/src/routes/internal/ehr/canvas/setup.ts
packages/api/src/command/internal-server/update-customer.ts
packages/api/src/routes/internal/ehr/canvas/index.ts
packages/api/src/command/internal-server/report-usage.ts
packages/core/src/external/internal-server/client.ts
packages/api/src/command/medical/document/document-webhook.ts
packages/api/src/routes/middlewares/usage.ts
packages/api/src/command/webhook/devices.ts
🧠 Learnings (4)
📚 Learning: 2025-06-06T16:45:31.832Z
Learnt from: thomasyopes
PR: metriport/metriport#3970
File: packages/api/src/external/ehr/athenahealth/command/write-back/medication.ts:17-17
Timestamp: 2025-06-06T16:45:31.832Z
Learning: The writeMedicationToChart function in packages/api/src/external/ehr/athenahealth/command/write-back/medication.ts returns a response that is not currently used by any consumers, so changes to its return type are not breaking changes in practice.
Applied to files:
packages/api/src/command/medical/document/__tests__/process-doc-query-webhook.test.ts
packages/api/src/command/medical/document/document-webhook.ts
📚 Learning: 2025-05-20T21:26:26.804Z
Learnt from: leite08
PR: metriport/metriport#3814
File: packages/api/src/routes/internal/medical/patient-consolidated.ts:141-174
Timestamp: 2025-05-20T21:26:26.804Z
Learning: The functionality introduced in packages/api/src/routes/internal/medical/patient-consolidated.ts is planned to be refactored in downstream PR #3857, including improvements to error handling and validation.
Applied to files:
packages/api/src/command/medical/document/__tests__/process-doc-query-webhook.test.ts
packages/api/src/routes/internal/ehr/canvas/setup.ts
packages/api/src/routes/internal/ehr/canvas/index.ts
packages/api/src/command/medical/document/document-webhook.ts
📚 Learning: 2025-06-13T20:24:16.668Z
Learnt from: thomasyopes
PR: metriport/metriport#3997
File: packages/api/src/routes/internal/medical/patient-job.ts:28-29
Timestamp: 2025-06-13T20:24:16.668Z
Learning: In packages/api/src/routes/internal/medical/patient-job.ts, mounting patientJobsRouter at the root path before other route handlers is intentional behavior, even though it could potentially consume requests before later handlers.
Applied to files:
packages/api/src/routes/internal/ehr/canvas/index.ts
📚 Learning: 2025-03-05T21:42:32.929Z
Learnt from: thomasyopes
PR: metriport/metriport#3383
File: packages/api/src/routes/ehr-internal/canvas/internal/patient.ts:10-24
Timestamp: 2025-03-05T21:42:32.929Z
Learning: The processPatientsFromAppointments endpoint in the Canvas API is intentionally designed to be asynchronous (fire-and-forget pattern). The endpoint returns a 200 OK response immediately after initiating the process, without waiting for it to complete. Error handling is handled through the .catch() method with processAsyncError rather than awaiting the promise.
Applied to files:
packages/api/src/command/medical/document/document-webhook.ts
🧬 Code Graph Analysis (6)
packages/core/src/util/config.ts (1)
packages/shared/src/common/env-var.ts (1)
getEnvVarOrFail
(14-20)
packages/api/src/shared/config.ts (1)
packages/shared/src/common/env-var.ts (2)
getEnvVarOrFail
(14-20)getEnvVar
(10-12)
packages/api/src/routes/internal/ehr/canvas/setup.ts (1)
packages/api/src/command/internal-server/update-customer.ts (1)
updateCustomerBillingToPointToParent
(9-19)
packages/api/src/command/internal-server/update-customer.ts (3)
packages/api/src/shared/config.ts (1)
Config
(4-322)packages/core/src/util/config.ts (1)
Config
(10-403)packages/core/src/external/internal-server/client.ts (1)
InternalServerApi
(7-32)
packages/api/src/command/internal-server/report-usage.ts (3)
packages/api/src/shared/config.ts (1)
Config
(4-322)packages/core/src/external/internal-server/client.ts (1)
InternalServerApi
(7-32)packages/lambdas/src/shared/capture.ts (1)
capture
(19-118)
packages/api/src/routes/middlewares/usage.ts (1)
packages/api/src/command/internal-server/report-usage.ts (1)
reportUsage
(14-26)
🔇 Additional comments (8)
packages/api/src/shared/config.ts (1)
199-202
: Getter for CX_BILLING_URL — LGTMOptional env accessor is consistent with usage.
packages/api/src/routes/internal/ehr/canvas/setup.ts (1)
19-32
: Confirmed: route is mounted under /internal (infra-protected) — no app-level auth appliedThe POST handler lives at packages/api/src/routes/internal/ehr/canvas/setup.ts (POST "/" → full path /internal/ehr/canvas/setup). It is mounted via:
- packages/api/src/routes/internal/index.ts — router.use("/ehr", ehr)
- packages/api/src/routes/index.ts — app.use("/internal", internal) (note comment: "internal only routes, should be disabled at API Gateway")
There is no application-level auth middleware on the "/internal" mount; this matches the project convention (internal routes are intended to be restricted at the API Gateway/VPC level and therefore do not use app-level authorization).
packages/api/src/command/medical/document/document-webhook.ts (2)
48-56
: Switch to a named async function aligns with our guidelines.Replacing the exported arrow function with a named function improves readability and adheres to “avoid arrow functions” in our coding standards. No behavior change observed.
141-141
: Minor stylistic change acknowledged.Closing with "}" (instead of "};") matches the new declaration style; no functional impact.
packages/core/src/external/internal-server/client.ts (1)
10-15
: Good: centralized axios instance with baseURL + timeout.Centralizing internal-server calls behind a client is the right move and the 20s timeout is a reasonable default.
packages/api/src/routes/middlewares/usage.ts (2)
2-2
: Import path updated to internal-server/report-usage — good alignment.This matches the new InternalServerApi-backed reporting flow.
15-31
: Named export for middleware matches our style guide.Switching from an exported arrow to a named function is consistent with “avoid arrow functions”.
packages/api/src/command/webhook/devices.ts (1)
45-50
: Verify external webhook contract before addingconnectedDevices
todevices.provider-connected
payloadRepo search found only the event construction in packages/api/src/command/webhook/devices.ts (buildWebhookRequestData call, ~lines 69–74); no internal schema/validator/docs referencing
devices.provider-connected
were found.
- Location to review: packages/api/src/command/webhook/devices.ts — payload/type: "devices.provider-connected" (around lines 69–74).
Action: If an external contract (OpenAPI/JSON Schema/docs) or external consumers exist, update the contract or coordinate the change so
connectedDevices
is accepted; otherwise this additive change is likely safe internally but still requires confirmation from webhook consumers.
packages/api/src/command/medical/document/__tests__/process-doc-query-webhook.test.ts
Show resolved
Hide resolved
@@ -31,6 +31,7 @@ services: | |||
- DYNAMODB_ENDPOINT=http://dynamodb:8000 | |||
- FHIR_SERVER_URL=http://host.docker.internal:8888 | |||
- FHIR_CONVERTER_SERVER_URL=http://host.docker.internal:8777 | |||
- INTERNAL_SERVER_BASE_URL=http://host.docker.internal:8090 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add "Add new env params for usage / billing urls to .env in 1PW" as a deployment action in the PR description -- hard to track cause the .env isn't in source control
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
packages/api/src/command/internal-server/report-usage.ts
(1 hunks)packages/api/src/command/internal-server/update-customer.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/api/src/command/internal-server/update-customer.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{js,jsx,ts,tsx}
: Don’t use null inside the app, only on code interacting with external interfaces/services, like DB and HTTP; convert to undefined before sending inwards into the code
Useconst
whenever possible
Useasync/await
instead of.then()
Naming: classes, enums: PascalCase
Naming: constants, variables, functions: camelCase
Naming: file names: kebab-case
Naming: Don’t use negative names, likenotEnabled
, preferisDisabled
If possible, use decomposing objects for function parameters
Prefer Nullish Coalesce (??) than the OR operator (||) when you want to provide a default value
Avoid creating arrow functions
Use truthy syntax instead ofin
- i.e.,if (data.link)
notif ('link' in data)
While handling errors, keep the stack trace around: if you create a new Error (e.g., MetriportError), make sure to pass the original error as the new one’s cause so the stack trace is available upstream.
max column length is 100 chars
multi-line comments use/** */
top-level comments go after the import (save pre-import to basic file header, like license)
move literals to constants declared after imports when possible
Files:
packages/api/src/command/internal-server/report-usage.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
Use types whenever possible
Files:
packages/api/src/command/internal-server/report-usage.ts
**/*.ts
⚙️ CodeRabbit configuration file
**/*.ts
: - Use the Onion Pattern to organize a package's code in layers
- Try to use immutable code and avoid sharing state across different functions, objects, and systems
- Try to build code that's idempotent whenever possible
- Prefer functional programming style functions: small, deterministic, 1 input, 1 output
- Minimize coupling / dependencies
- Avoid modifying objects received as parameter
- Only add comments to code to explain why something was done, not how it works
- Naming
- classes, enums:
PascalCase
- constants, variables, functions:
camelCase
- file names:
kebab-case
- table and column names:
snake_case
- Use meaningful names, so whoever is reading the code understands what it means
- Don’t use negative names, like
notEnabled
, preferisDisabled
- For numeric values, if the type doesn’t convey the unit, add the unit to the name
- Typescript
- Use types
- Prefer
const
instead oflet
- Avoid
any
and casting fromany
to other types- Type predicates: only applicable to narrow down the type, not to force a complete type conversion
- Prefer deconstructing parameters for functions instead of multiple parameters that might be of
the same type- Don’t use
null
inside the app, only on code interacting with external interfaces/services,
like DB and HTTP; convert toundefined
before sending inwards into the code- Use
async/await
instead of.then()
- Use the strict equality operator
===
, don’t use abstract equality operator==
- When calling a Promise-returning function asynchronously (i.e., not awaiting), use
.catch()
to
handle errors (seeprocessAsyncError
andemptyFunction
depending on the case)- Date and Time
- Always use
buildDayjs()
to createdayjs
instances- Prefer
dayjs.duration(...)
to create duration consts and keep them asduration
- Prefer Nullish Coalesce (??) than the OR operator (||) to provide a default value
- Avoid creating arrow functions
- U...
Files:
packages/api/src/command/internal-server/report-usage.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: alexeysergeev-cm
PR: metriport/metriport#4367
File: packages/api/docker-compose.dev.yml:34-34
Timestamp: 2025-08-14T20:13:35.367Z
Learning: In packages/api/docker-compose.dev.yml, CX_BILLING_URL environment variable is not required for development purposes. Only INTERNAL_SERVER_BASE_URL is needed for dev inter-server communication, even though the code has getCxBillingurl("") functionality that may throw if undefined.
🧬 Code graph analysis (1)
packages/api/src/command/internal-server/report-usage.ts (5)
packages/api/src/routes/middlewares/usage.ts (1)
reportUsage
(15-31)packages/api/src/shared/config.ts (1)
Config
(4-318)packages/core/src/util/config.ts (1)
Config
(10-403)packages/core/src/external/internal-server/client.ts (1)
InternalServerApi
(8-37)packages/lambdas/src/shared/capture.ts (1)
capture
(19-118)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (javascript)
🔇 Additional comments (3)
packages/api/src/command/internal-server/report-usage.ts (3)
1-2
: LGTM: move to InternalServerApi + Core Config import is appropriate.
15-15
: LGTM: use of a named function aligns with “avoid arrow functions”.
24-24
: Verify USAGE_URL values before routing via InternalServerApi
No static literals for USAGE_URL starting withhttp://
,https://
, or/
were found in the repo—props.config.usageReportUrl may still be an absolute URL at runtime. Confirm that USAGE_URL values are always relative paths, or apply the normalization snippet (extracting path and query from any absolute URL) before callinginternalServerApi.makeRequest
to prevent bypassing the client’s baseURL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good 🚀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (1)
packages/api/src/routes/internal/ehr/canvas/setup.ts (1)
47-52
: Guard billing update when CX_BILLING_URL is absent; align with dev setup.Defaulting isTenant to true triggers billing even in dev, where CX_BILLING_URL is intentionally unset (per learning). Skip the call when the URL isn’t configured.
Apply:
- const isTenant = getFromQuery("isTenant", req) !== "false"; // defaults to true + const isTenantStr = getFromQuery("isTenant", req); + const isTenant = (isTenantStr ?? "true") === "true"; // explicit nullish default @@ - if (isTenant) { - await updateCustomerBillingToPointToParent({ parentName: EhrSources.canvas, childCxId }); - } + if (isTenant) { + if (process.env.CX_BILLING_URL) { + await updateCustomerBillingToPointToParent({ parentName: EhrSources.canvas, childCxId }); + } else { + // dev: CX_BILLING_URL not configured -> skip billing update + } + }This honors dev docker-compose where only INTERNAL_SERVER_BASE_URL is required.
🧹 Nitpick comments (3)
packages/api/src/routes/internal/ehr/canvas/setup.ts (3)
21-24
: Extract token length to a named constant.Avoid magic number 30 and centralize token generation.
Apply:
- function generateToken(): string { - return nanoid(30); - } + const TOKEN_SIZE = 30; + function generateToken(): string { + return nanoid(TOKEN_SIZE); + }
54-67
: Parallelize independent mapping operations.These DB calls don’t depend on each other; do them concurrently to cut latency.
Apply:
- await findOrCreateFacilityMapping({ - cxId: childCxId, - facilityId: facilityId, - externalId, - source: EhrSources.canvas, - }); - - await findOrCreateCxMapping({ - cxId: childCxId, - source: EhrSources.canvas, - externalId, - secondaryMappings: {}, - }); + await Promise.all([ + findOrCreateFacilityMapping({ + cxId: childCxId, + facilityId, + externalId, + source: EhrSources.canvas, + }), + findOrCreateCxMapping({ + cxId: childCxId, + source: EhrSources.canvas, + externalId, + secondaryMappings: {}, + }), + ]);
68-71
: Clarify variable semantics.Name the variable by meaning, not duration.
Apply:
- const twoYearsFromNow = getDefaultExpiration(); + const expiresAtMs = getDefaultExpiration();And update exp usages below accordingly.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
packages/api/src/routes/internal/ehr/canvas/setup.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{js,jsx,ts,tsx}
: Don’t use null inside the app, only on code interacting with external interfaces/services, like DB and HTTP; convert to undefined before sending inwards into the code
Useconst
whenever possible
Useasync/await
instead of.then()
Naming: classes, enums: PascalCase
Naming: constants, variables, functions: camelCase
Naming: file names: kebab-case
Naming: Don’t use negative names, likenotEnabled
, preferisDisabled
If possible, use decomposing objects for function parameters
Prefer Nullish Coalesce (??) than the OR operator (||) when you want to provide a default value
Avoid creating arrow functions
Use truthy syntax instead ofin
- i.e.,if (data.link)
notif ('link' in data)
While handling errors, keep the stack trace around: if you create a new Error (e.g., MetriportError), make sure to pass the original error as the new one’s cause so the stack trace is available upstream.
max column length is 100 chars
multi-line comments use/** */
top-level comments go after the import (save pre-import to basic file header, like license)
move literals to constants declared after imports when possible
Files:
packages/api/src/routes/internal/ehr/canvas/setup.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
Use types whenever possible
Files:
packages/api/src/routes/internal/ehr/canvas/setup.ts
**/*.ts
⚙️ CodeRabbit configuration file
**/*.ts
: - Use the Onion Pattern to organize a package's code in layers
- Try to use immutable code and avoid sharing state across different functions, objects, and systems
- Try to build code that's idempotent whenever possible
- Prefer functional programming style functions: small, deterministic, 1 input, 1 output
- Minimize coupling / dependencies
- Avoid modifying objects received as parameter
- Only add comments to code to explain why something was done, not how it works
- Naming
- classes, enums:
PascalCase
- constants, variables, functions:
camelCase
- file names:
kebab-case
- table and column names:
snake_case
- Use meaningful names, so whoever is reading the code understands what it means
- Don’t use negative names, like
notEnabled
, preferisDisabled
- For numeric values, if the type doesn’t convey the unit, add the unit to the name
- Typescript
- Use types
- Prefer
const
instead oflet
- Avoid
any
and casting fromany
to other types- Type predicates: only applicable to narrow down the type, not to force a complete type conversion
- Prefer deconstructing parameters for functions instead of multiple parameters that might be of
the same type- Don’t use
null
inside the app, only on code interacting with external interfaces/services,
like DB and HTTP; convert toundefined
before sending inwards into the code- Use
async/await
instead of.then()
- Use the strict equality operator
===
, don’t use abstract equality operator==
- When calling a Promise-returning function asynchronously (i.e., not awaiting), use
.catch()
to
handle errors (seeprocessAsyncError
andemptyFunction
depending on the case)- Date and Time
- Always use
buildDayjs()
to createdayjs
instances- Prefer
dayjs.duration(...)
to create duration consts and keep them asduration
- Prefer Nullish Coalesce (??) than the OR operator (||) to provide a default value
- Avoid creating arrow functions
- U...
Files:
packages/api/src/routes/internal/ehr/canvas/setup.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: alexeysergeev-cm
PR: metriport/metriport#4367
File: packages/api/docker-compose.dev.yml:34-34
Timestamp: 2025-08-14T20:13:35.367Z
Learning: In packages/api/docker-compose.dev.yml, CX_BILLING_URL environment variable is not required for development purposes. Only INTERNAL_SERVER_BASE_URL is needed for dev inter-server communication, even though the code has getCxBillingurl("") functionality that may throw if undefined.
🧬 Code graph analysis (1)
packages/api/src/routes/internal/ehr/canvas/setup.ts (2)
packages/api/src/command/internal-server/update-customer.ts (1)
updateCustomerBillingToPointToParent
(10-21)packages/api/src/command/mapping/cx.ts (1)
findOrCreateCxMapping
(27-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Analyze (javascript)
🔇 Additional comments (2)
packages/api/src/routes/internal/ehr/canvas/setup.ts (2)
40-44
: Secure the internal endpoint (missing auth).This route is unauthenticated and returns privileged tokens. Add internal-only auth (shared secret, mTLS, IP allowlist) as middleware.
Apply middleware in the chain:
router.post( "/", - requestLogger, + requestLogger, + internalAuth, // verify internal secret/mTLS/allowlist asyncHandler(async (req: Request, res: Response) => {Outside the selected range, add a proper import:
// choose the correct path in your codebase import { internalAuth } from "../../../helpers/internal-auth";⛔ Skipped due to learnings
Learnt from: alexeysergeev-cm PR: metriport/metriport#4367 File: packages/api/src/routes/internal/ehr/canvas/index.ts:3-8 Timestamp: 2025-08-14T20:17:36.570Z Learning: In the Metriport API codebase, internal routes (mounted under /internal) rely on API Gateway for access control rather than having code-level internal auth middleware. This architectural pattern is intentional and expected.
Learnt from: thomasyopes PR: metriport/metriport#3466 File: packages/api/src/routes/internal/internal.ts:0-0 Timestamp: 2025-03-17T23:45:45.512Z Learning: The internal.ts file in packages/api/src/routes/internal/ is excluded from PR changes related to replacing console.log with out().log.
72-92
: Verify token payload fields, expiration unit, and logging
- Payloads match their Zod schemas: dash token includes only
practiceId
/source
, webhook addscxId
(no need to mirror fields).- Confirm
getDefaultExpiration()
(ms since epoch) aligns with your DB model’sexp
type and isn’t off by a factor of 1,000.- Ensure
requestLogger
(or any middleware) never logs the rawdashToken
orwebhookToken
.
// Calculate expiration (2 years from now in milliseconds) | ||
function getDefaultExpiration(): number { | ||
return dayjs().add(2, "year").valueOf(); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Use buildDayjs() and a duration constant; verify exp units.
Follow project time guidelines and avoid magic numbers. Also confirm whether saveJwtToken expects seconds or milliseconds for exp.
Apply:
-// Calculate expiration (2 years from now in milliseconds)
-function getDefaultExpiration(): number {
- return dayjs().add(2, "year").valueOf();
-}
+// Calculate expiration using a duration; returns epoch ms
+const TOKEN_TTL = dayjs.duration({ years: 2 });
+function getDefaultExpiration(): number {
+ return buildDayjs().add(TOKEN_TTL).valueOf();
+}
Outside the selected range, add (adjust path as needed):
import { buildDayjs } from "@metriport/shared/utils/dayjs";
Run to verify exp units:
🏁 Script executed:
#!/bin/bash
# Inspect saveJwtToken signature/usage to confirm exp units (ms vs s).
rg -nP -C3 'saveJwtToken\s*\('
rg -nP -C3 'interface\s+\w*JwtToken|type\s+\w*JwtToken'
rg -nP -C3 '\bexp\b'
Length of output: 40997
🏁 Script executed:
#!/bin/bash
# Find where buildDayjs is imported to confirm path
rg -nP -C3 'import.*buildDayjs' .
# Locate token duration constants for reuse
rg -nP 'shortDurationTokenDuration' -n 50 .
rg -nP 'longDurationTokenDuration' -n 50 .
Length of output: 50275
Extract a named 2-year TTL and use buildDayjs(); keep exp in ms
Replace the magic “2 years” call with a constant and switch to buildDayjs for consistency. For example:
+ import { buildDayjs } from "@metriport/shared/common/date";
import dayjs from "dayjs";
+ import duration from "dayjs/plugin/duration";
dayjs.extend(duration);
- // Calculate expiration (2 years from now in milliseconds)
- function getDefaultExpiration(): number {
- return dayjs().add(2, "year").valueOf();
- }
+ // Two-year token TTL
+ const CANVAS_TOKEN_TTL = dayjs.duration({ years: 2 });
+ function getDefaultExpiration(): number {
+ return buildDayjs().add(CANVAS_TOKEN_TTL).valueOf();
+ }
saveJwtToken’s exp parameter is treated as milliseconds (it wraps new Date(exp)
), so no unit change is needed.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Calculate expiration (2 years from now in milliseconds) | |
function getDefaultExpiration(): number { | |
return dayjs().add(2, "year").valueOf(); | |
} | |
import { buildDayjs } from "@metriport/shared/common/date"; | |
import dayjs from "dayjs"; | |
import duration from "dayjs/plugin/duration"; | |
dayjs.extend(duration); | |
// Two-year token TTL | |
const CANVAS_TOKEN_TTL = dayjs.duration({ years: 2 }); | |
function getDefaultExpiration(): number { | |
return buildDayjs().add(CANVAS_TOKEN_TTL).valueOf(); | |
} |
🤖 Prompt for AI Agents
In packages/api/src/routes/internal/ehr/canvas/setup.ts around lines 25-29,
replace the magic "2 years" literal with a named constant (e.g.
TWO_YEAR_TTL_YEARS = 2) and use buildDayjs() instead of dayjs() to construct the
date; compute expiration as buildDayjs().add(TWO_YEAR_TTL_YEARS,
"year").valueOf() so the returned exp remains in milliseconds (no unit change).
// TODO: ENG-877 - Update AWS secrets within endpoint for Canvas cx setup | ||
// Place these tokens in a secure note in 1PW | ||
return res.status(httpStatus.OK).json({ dashToken, webhookToken }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Don’t persist secrets in 1Password via code path; automate secure storage.
Prefer storing generated tokens in a programmatic secret store (e.g., AWS Secrets Manager/Parameter Store) with strict IAM, audit, rotation, and TTL, not 1Password notes.
metriport/metriport-internal#1040 Ref: Signed-off-by: Alexey Sergeev <alexey@metriport.com>
0aca87e
to
8a0b55d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/api/src/routes/middlewares/usage.ts (1)
39-54
: Handle fire-and-forget promise with .catch() to avoid unhandled rejections.Guideline: when not awaiting a Promise, chain .catch().
-async function reportIt( - req: Request, - product: Product, - entityId: string | undefined -): Promise<void> { +function reportIt(req: Request, product: Product, entityId: string | undefined): void { const cxId = getCxId(req); if (!cxId) { log(`Skipped, missing cxId`); return; } if (!entityId) { log(`Skipped, missing entityId`); return; } - reportUsageCmd({ cxId, entityId, product }); + void reportUsageCmd({ cxId, entityId, product }).catch(err => + log( + `Failed reporting usage - product=${product} cxId=${cxId} entityId=${entityId}: ${ + err instanceof Error ? err.message : String(err) + }` + ) + ); }
♻️ Duplicate comments (3)
packages/api/src/routes/internal/ehr/canvas/setup.ts (3)
72-92
: Double-check exp units expected by saveJwtToken.Confirm exp is epoch ms (not seconds). Prior analysis said saveJwtToken wraps new Date(exp), implying ms; if that changed, adjust.
I can run a quick repo scan script to verify current saveJwtToken signature/usages if you want.
6-7
: Use buildDayjs() and extract a TTL constant (no magic “2 years”); keep exp in epoch ms.Aligns with project date/time guidelines and avoids magic numbers. Also ensures consistency across services.
-import dayjs from "dayjs"; +import dayjs from "dayjs"; +import { buildDayjs } from "@metriport/shared/utils/dayjs"; +import duration from "dayjs/plugin/duration"; +dayjs.extend(duration); @@ -// Calculate expiration (2 years from now in milliseconds) -function getDefaultExpiration(): number { - return dayjs().add(2, "year").valueOf(); -} +// Two-year TTL expressed as milliseconds +const CANVAS_TOKEN_TTL_MS = dayjs.duration({ years: 2 }).asMilliseconds(); +function getDefaultExpiration(): number { + return buildDayjs().add(CANVAS_TOKEN_TTL_MS, "millisecond").valueOf(); +}Also applies to: 25-28
94-96
: Don’t store secrets in 1Password via code path; use a managed secret store.Use AWS Secrets Manager/Parameter Store with IAM scoping, audit, rotation, and TTL. Return tokens here only when strictly necessary, and store them programmatically.
🧹 Nitpick comments (6)
packages/api/src/routes/internal/ehr/canvas/setup.ts (2)
47-47
: Parse isTenant explicitly; current check treats any non-"false" as true.Avoid surprising truthiness from values like "0" or "False".
-const isTenant = getFromQuery("isTenant", req) !== "false"; // defaults to true +const isTenantRaw = getFromQuery("isTenant", req); +const isTenant = isTenantRaw ? isTenantRaw === "true" : true; // defaults to true
68-71
: Token issuance strategy: avoid unlimited token proliferation.Every call issues fresh tokens; consider rotation/reuse semantics to prevent multiple active tokens per practice.
Questions:
- Should we reuse existing tokens for the same practiceId/cxId if present?
- If rotating, should we revoke old tokens atomically?
I can draft a small helper to upsert by (source, practiceId[, cxId]) if that matches your data model.
packages/api/src/routes/middlewares/usage.ts (2)
15-31
: Prefer named middleware function and proper Response typing; drop the arrow.Matches code style (“avoid arrow functions”) and removes
any
.-import { NextFunction, Request } from "express"; +import { NextFunction, Request, Response } from "express"; @@ -export function reportUsage(product: Product, getEntityIdFn: (req: Request) => string | undefined) { - return async ( - req: Request, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - res: any, // otherwise we get type error, those Response functions are not mapped on Typescript - next: NextFunction - ): Promise<void> => { +export function reportUsage( + product: Product, + getEntityIdFn: (req: Request) => string | undefined +) { + return function usageMiddleware(req: Request, res: Response, next: NextFunction): void { function afterResponse() { res.removeListener("finish", afterResponse); res.removeListener("close", afterResponse); - if (isHttpOK(res.statusCode)) reportIt(req, product, getEntityIdFn(req)); + if (isHttpOK(res.statusCode)) { + // fire-and-forget + reportIt(req, product, getEntityIdFn(req)); + } } res.on("finish", afterResponse); res.on("close", afterResponse); next(); - }; + }; }Also applies to: 1-1
34-36
: Fix small doc grammar.- * Reports usage base on the the customer ID on the Request, property 'cxId', and + * Reports usage based on the customer ID on the Request ('cxId') andpackages/api/src/command/webhook/devices.ts (2)
86-90
: Tidy comment typo.- * programatically . + * programmatically.
111-115
: Use errorToString for consistent error logging.- `error: ${error}` + `error: ${errorToString(error)}`
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (10)
packages/api/docker-compose.dev.yml
(1 hunks)packages/api/src/command/internal-server/report-usage.ts
(1 hunks)packages/api/src/command/internal-server/update-customer.ts
(1 hunks)packages/api/src/command/medical/document/__tests__/process-doc-query-webhook.test.ts
(2 hunks)packages/api/src/command/medical/document/document-webhook.ts
(3 hunks)packages/api/src/command/webhook/devices.ts
(6 hunks)packages/api/src/routes/internal/ehr/canvas/index.ts
(1 hunks)packages/api/src/routes/internal/ehr/canvas/setup.ts
(1 hunks)packages/api/src/routes/middlewares/usage.ts
(4 hunks)packages/api/src/shared/config.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
- packages/api/src/routes/internal/ehr/canvas/index.ts
- packages/api/src/shared/config.ts
- packages/api/src/command/medical/document/tests/process-doc-query-webhook.test.ts
- packages/api/src/command/internal-server/update-customer.ts
- packages/api/docker-compose.dev.yml
- packages/api/src/command/internal-server/report-usage.ts
- packages/api/src/command/medical/document/document-webhook.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{js,jsx,ts,tsx}
: Don’t use null inside the app, only on code interacting with external interfaces/services, like DB and HTTP; convert to undefined before sending inwards into the code
Useconst
whenever possible
Useasync/await
instead of.then()
Naming: classes, enums: PascalCase
Naming: constants, variables, functions: camelCase
Naming: file names: kebab-case
Naming: Don’t use negative names, likenotEnabled
, preferisDisabled
If possible, use decomposing objects for function parameters
Prefer Nullish Coalesce (??) than the OR operator (||) when you want to provide a default value
Avoid creating arrow functions
Use truthy syntax instead ofin
- i.e.,if (data.link)
notif ('link' in data)
While handling errors, keep the stack trace around: if you create a new Error (e.g., MetriportError), make sure to pass the original error as the new one’s cause so the stack trace is available upstream.
max column length is 100 chars
multi-line comments use/** */
top-level comments go after the import (save pre-import to basic file header, like license)
move literals to constants declared after imports when possible
Files:
packages/api/src/routes/internal/ehr/canvas/setup.ts
packages/api/src/routes/middlewares/usage.ts
packages/api/src/command/webhook/devices.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
Use types whenever possible
**/*.{ts,tsx}
: Prefer functional programming style functions: small, deterministic, single input and output
Avoid modifying objects received as parameters
Naming: classes and enums use PascalCase
Naming: constants, variables, and functions use camelCase
File names use kebab-case
Avoid negative names (e.g., prefer isDisabled to notEnabled)
Include units in numeric variable names when the type doesn’t convey the unit
TypeScript: use explicit types
TypeScript: prefer const over let
TypeScript: avoid any and casting from any to other types
TypeScript: use type predicates only for narrowing, not for full type coercion
TypeScript: prefer destructuring function parameters instead of multiple same-typed parameters
TypeScript: don’t use null inside the app; only at external boundaries and convert to undefined internally
TypeScript: use async/await instead of .then()
TypeScript: use strict equality ===; do not use ==
When calling a Promise-returning function without awaiting, chain .catch() to handle errors
Date/Time: always use buildDayjs() to create dayjs instances
Date/Time: prefer dayjs.duration(...) and keep values as duration
Prefer nullish coalescing (??) over OR (||) for default values
Avoid creating arrow functions
Use truthy property checks instead of the in operator (e.g., if (data.link))
Error handling: pass the original error as cause when wrapping errors
Error handling: use static error messages; put dynamic data in MetriportError.additionalInfo; error object last parameter
Global constants: move literals to constants after imports (avoid magic numbers)
Avoid multi-line logs: do not pass objects as the second parameter to console.log() or out().log()
Avoid multi-line logs: do not create multi-line strings with JSON.stringify()
Max column length is 100 characters
Use multi-line comments with /** */
Files:
packages/api/src/routes/internal/ehr/canvas/setup.ts
packages/api/src/routes/middlewares/usage.ts
packages/api/src/command/webhook/devices.ts
🧠 Learnings (5)
📓 Common learnings
Learnt from: alexeysergeev-cm
PR: metriport/metriport#4367
File: packages/api/docker-compose.dev.yml:34-34
Timestamp: 2025-08-14T20:13:35.367Z
Learning: In packages/api/docker-compose.dev.yml, CX_BILLING_URL environment variable is not required for development purposes. Only INTERNAL_SERVER_BASE_URL is needed for dev inter-server communication, even though the code has getCxBillingurl("") functionality that may throw if undefined.
📚 Learning: 2025-09-01T19:51:11.032Z
Learnt from: CR
PR: metriport/metriport#0
File: CONVENTIONS.md:0-0
Timestamp: 2025-09-01T19:51:11.032Z
Learning: Applies to **/*.{ts,tsx} : Date/Time: prefer dayjs.duration(...) and keep values as duration
Applied to files:
packages/api/src/routes/internal/ehr/canvas/setup.ts
📚 Learning: 2025-05-30T01:09:55.013Z
Learnt from: keshavsaharia
PR: metriport/metriport#3885
File: packages/core/src/external/surescripts/client.ts:0-0
Timestamp: 2025-05-30T01:09:55.013Z
Learning: In packages/core/src/external/surescripts/client.ts, the Surescripts date formatting should use local time according to their documentation ("message-date-must-be-local"), which means buildDayjs() cannot be used since it defaults to UTC. The specific use case for transmission date formatting requires dayjs to be used directly to preserve local time behavior.
Applied to files:
packages/api/src/routes/internal/ehr/canvas/setup.ts
📚 Learning: 2025-09-01T19:51:11.032Z
Learnt from: CR
PR: metriport/metriport#0
File: CONVENTIONS.md:0-0
Timestamp: 2025-09-01T19:51:11.032Z
Learning: Applies to **/*.{ts,tsx} : Date/Time: always use buildDayjs() to create dayjs instances
Applied to files:
packages/api/src/routes/internal/ehr/canvas/setup.ts
📚 Learning: 2025-03-10T22:17:43.667Z
Learnt from: lucasdellabella
PR: metriport/metriport#3440
File: .sentryclirc:0-0
Timestamp: 2025-03-10T22:17:43.667Z
Learning: Sentry tokens and other sensitive credentials should not be stored in the repository, even if they are depermissioned or inactive. Best practice is to use environment variables or a secrets management solution.
Applied to files:
packages/api/src/routes/internal/ehr/canvas/setup.ts
🧬 Code graph analysis (1)
packages/api/src/routes/internal/ehr/canvas/setup.ts (1)
packages/api/src/command/mapping/cx.ts (1)
findOrCreateCxMapping
(27-43)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: check-pr / lint-build-test
- GitHub Check: check-pr / lint-build-test
- GitHub Check: check-pr / lint-build-test
- GitHub Check: Analyze (javascript)
🔇 Additional comments (3)
packages/api/src/routes/internal/ehr/canvas/setup.ts (2)
40-44
: Missing internal auth on an endpoint that returns secrets (tokens).This internal route exposes tokens but lacks explicit authentication/authorization middleware here. Ensure it’s gated (mTLS, allowlist, or signed internal key) before merge.
Example (replace with your actual internal auth middleware):
router.post( "/", requestLogger, + requireInternalAuth, asyncHandler(async (req: Request, res: Response) => {
⛔ Skipped due to learnings
Learnt from: alexeysergeev-cm PR: metriport/metriport#4367 File: packages/api/src/routes/internal/ehr/canvas/index.ts:3-8 Timestamp: 2025-08-14T20:17:36.570Z Learning: In the Metriport API codebase, internal routes (mounted under /internal) rely on API Gateway for access control rather than having code-level internal auth middleware. This architectural pattern is intentional and expected.
49-53
: Guard billing update in development
WrapupdateCustomerBillingToPointToParent
in a try/catch so missing CX_BILLING_URL only aborts in non-dev environments:if (isTenant) { - await updateCustomerBillingToPointToParent({ parentName: EhrSources.canvas, childCxId }); + try { + await updateCustomerBillingToPointToParent({ parentName: EhrSources.canvas, childCxId }); + } catch (err) { + if (process.env.NODE_ENV !== "development") throw err; + console.warn( + `Skipping billing update in development: ${ + err instanceof Error ? err.message : String(err) + }` + ); + } }packages/api/src/routes/middlewares/usage.ts (1)
2-2
: Import switch to internal-server/report-usage looks good.
metriport/metriport-internal#1040 Ref: Signed-off-by: Alexey Sergeev <alexey@metriport.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (3)
packages/api/src/command/internal-server/report-usage.ts (2)
15-16
: Emit one lightweight telemetry message instead of silently no-op when USAGE_URL is missing.Avoid silent returns; log once to Sentry so misconfig isn’t invisible. Also rename variable to path for clarity with InternalServerApi.
Apply:
- const url = Config.getUsageurl(""); - if (!url) return; + const path = Config.getUsageurl(""); + if (!path) { + if (!warnedMissingUsageUrl) { + warnedMissingUsageUrl = true; + capture.message("Usage reporting disabled due to missing USAGE_URL.", { + extra: { cxId, entityId, product }, + }); + } + return; + }Add (outside the function, after imports):
let warnedMissingUsageUrl = false;
21-24
: Remove console.log; rely on capture.error only (per guidelines).Console usage isn’t allowed in api; Sentry already captures context.
Apply:
- internalServerApi.makeRequest("POST", url, payload).catch(err => { - console.log(`Failed to report usage (${stringify(payload)}) to server ${url}: `, err.message); - capture.error(err, { extra: { payload, err } }); - }); + internalServerApi.makeRequest("POST", path, payload).catch(err => { + capture.error(err, { extra: { payload, url: path } }); + });And remove the now-unused import:
-import stringify from "json-stringify-safe";
packages/core/src/external/internal-server/client.ts (1)
28-45
: Fail fast on relative paths without baseURL and prevent additionalConfig from clobbering method/url/data.Adds clear error for misconfig and ensures call-site params win.
Apply:
async makeRequest<T = unknown>( method: "GET" | "POST" | "PUT" | "DELETE", path: string, data?: unknown, additionalConfig?: AxiosRequestConfig ): Promise<T> { - // disabled in sandbox environment - if (isSandbox) return Promise.resolve({} as T); + // disabled in sandbox environment + if (Config.isSandbox()) return Promise.resolve({} as T); + + const base = this.client.defaults.baseURL; + const isAbsolute = /^https?:\/\//i.test(path); + if ((!base || base.trim() === "") && !isAbsolute) { + throw new MetriportError( + "InternalServerApi baseURL is missing and a relative path was provided. " + + "Set INTERNAL_SERVER_BASE_URL or pass an absolute URL." + ); + } - const response = await this.client.request({ - method, - url: path, - data, - ...additionalConfig, - }); + const response = await this.client.request({ + ...additionalConfig, + method, + url: path, + data, + }); return response.data; }
🧹 Nitpick comments (3)
packages/api/src/command/internal-server/report-usage.ts (1)
18-18
: Consider reusing a single InternalServerApi instance.Instantiating per call is unnecessary overhead; cache a singleton.
Apply within this file:
- const internalServerApi = new InternalServerApi(); + const internalServerApi = getInternalServerApi();Add (top-level helper):
let cachedClient: InternalServerApi | undefined; function getInternalServerApi(): InternalServerApi { return (cachedClient ??= new InternalServerApi()); }packages/core/src/external/internal-server/client.ts (2)
5-6
: Don’t cache Config at module load; read it on demand.Top-level reads make tests/env switching brittle. Resolve in constructor/method instead.
Apply:
-const isSandbox = Config.isSandbox(); -const baseURL = Config.getInternalServerurl(""); +const TIMEOUT_MS = 20_000;
13-26
: Resolve baseURL and sandbox inside the constructor; lift timeout to a named constant.Prevents stale config and clarifies intent.
Apply:
constructor() { - // disabled in sandbox environment - if (isSandbox) { + // disabled in sandbox environment + if (Config.isSandbox()) { this.client = axios.create({ baseURL: "http://localhost" }); return; } - if (!baseURL) { + const baseURL = Config.getInternalServerurl(""); + if (!baseURL) { throw new MetriportError("INTERNAL_SERVER_BASE_URL not configured"); } this.client = axios.create({ - baseURL, - timeout: 20_000, + baseURL, + timeout: TIMEOUT_MS, }); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
packages/api/src/command/internal-server/report-usage.ts
(1 hunks)packages/core/src/external/internal-server/client.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{js,jsx,ts,tsx}
: Don’t use null inside the app, only on code interacting with external interfaces/services, like DB and HTTP; convert to undefined before sending inwards into the code
Useconst
whenever possible
Useasync/await
instead of.then()
Naming: classes, enums: PascalCase
Naming: constants, variables, functions: camelCase
Naming: file names: kebab-case
Naming: Don’t use negative names, likenotEnabled
, preferisDisabled
If possible, use decomposing objects for function parameters
Prefer Nullish Coalesce (??) than the OR operator (||) when you want to provide a default value
Avoid creating arrow functions
Use truthy syntax instead ofin
- i.e.,if (data.link)
notif ('link' in data)
While handling errors, keep the stack trace around: if you create a new Error (e.g., MetriportError), make sure to pass the original error as the new one’s cause so the stack trace is available upstream.
max column length is 100 chars
multi-line comments use/** */
top-level comments go after the import (save pre-import to basic file header, like license)
move literals to constants declared after imports when possible
Files:
packages/api/src/command/internal-server/report-usage.ts
packages/core/src/external/internal-server/client.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
Use types whenever possible
**/*.{ts,tsx}
: Prefer functional programming style functions: small, deterministic, single input and output
Avoid modifying objects received as parameters
Naming: classes and enums use PascalCase
Naming: constants, variables, and functions use camelCase
File names use kebab-case
Avoid negative names (e.g., prefer isDisabled to notEnabled)
Include units in numeric variable names when the type doesn’t convey the unit
TypeScript: use explicit types
TypeScript: prefer const over let
TypeScript: avoid any and casting from any to other types
TypeScript: use type predicates only for narrowing, not for full type coercion
TypeScript: prefer destructuring function parameters instead of multiple same-typed parameters
TypeScript: don’t use null inside the app; only at external boundaries and convert to undefined internally
TypeScript: use async/await instead of .then()
TypeScript: use strict equality ===; do not use ==
When calling a Promise-returning function without awaiting, chain .catch() to handle errors
Date/Time: always use buildDayjs() to create dayjs instances
Date/Time: prefer dayjs.duration(...) and keep values as duration
Prefer nullish coalescing (??) over OR (||) for default values
Avoid creating arrow functions
Use truthy property checks instead of the in operator (e.g., if (data.link))
Error handling: pass the original error as cause when wrapping errors
Error handling: use static error messages; put dynamic data in MetriportError.additionalInfo; error object last parameter
Global constants: move literals to constants after imports (avoid magic numbers)
Avoid multi-line logs: do not pass objects as the second parameter to console.log() or out().log()
Avoid multi-line logs: do not create multi-line strings with JSON.stringify()
Max column length is 100 characters
Use multi-line comments with /** */
Files:
packages/api/src/command/internal-server/report-usage.ts
packages/core/src/external/internal-server/client.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: alexeysergeev-cm
PR: metriport/metriport#4367
File: packages/api/docker-compose.dev.yml:34-34
Timestamp: 2025-08-14T20:13:35.367Z
Learning: In packages/api/docker-compose.dev.yml, CX_BILLING_URL environment variable is not required for development purposes. Only INTERNAL_SERVER_BASE_URL is needed for dev inter-server communication, even though the code has getCxBillingurl("") functionality that may throw if undefined.
📚 Learning: 2025-03-17T17:01:17.227Z
Learnt from: thomasyopes
PR: metriport/metriport#3466
File: packages/api/src/routes/ehr/shared.ts:122-147
Timestamp: 2025-03-17T17:01:17.227Z
Learning: Avoid using console.log and console.error in packages other than utils, infra and shared, and try to use out().log instead according to Metriport's coding guidelines.
Applied to files:
packages/api/src/command/internal-server/report-usage.ts
📚 Learning: 2025-03-17T23:45:45.512Z
Learnt from: thomasyopes
PR: metriport/metriport#3466
File: packages/api/src/routes/internal/internal.ts:0-0
Timestamp: 2025-03-17T23:45:45.512Z
Learning: The internal.ts file in packages/api/src/routes/internal/ is excluded from PR changes related to replacing console.log with out().log.
Applied to files:
packages/api/src/command/internal-server/report-usage.ts
📚 Learning: 2025-08-14T20:17:36.570Z
Learnt from: alexeysergeev-cm
PR: metriport/metriport#4367
File: packages/api/src/routes/internal/ehr/canvas/index.ts:3-8
Timestamp: 2025-08-14T20:17:36.570Z
Learning: In the Metriport API codebase, internal routes (mounted under /internal) rely on API Gateway for access control rather than having code-level internal auth middleware. This architectural pattern is intentional and expected.
Applied to files:
packages/core/src/external/internal-server/client.ts
🧬 Code graph analysis (2)
packages/api/src/command/internal-server/report-usage.ts (3)
packages/api/src/shared/config.ts (1)
Config
(4-322)packages/core/src/external/internal-server/client.ts (1)
InternalServerApi
(10-46)packages/lambdas/src/shared/capture.ts (1)
capture
(19-118)
packages/core/src/external/internal-server/client.ts (2)
packages/core/src/util/config.ts (2)
isSandbox
(25-27)Config
(11-408)packages/shared/src/index.ts (1)
MetriportError
(44-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: check-pr / lint-build-test
- GitHub Check: check-pr / lint-build-test
- GitHub Check: check-pr / lint-build-test
- GitHub Check: Analyze (javascript)
🔇 Additional comments (1)
packages/core/src/external/internal-server/client.ts (1)
13-18
: LGTM: sandbox short-circuit avoids noise.Constructor and method both no-op in sandbox, addressing earlier concerns about sandbox errors.
Also applies to: 34-36
metriport/metriport-internal#1040 Ref: Signed-off-by: Alexey Sergeev <alexey@metriport.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
packages/core/src/external/internal-server/client.ts (1)
11-15
: Good: fail-fast on missing base URL.This resolves the previous “relative path + missing baseURL” runtime error concern raised earlier. No further action needed here.
🧹 Nitpick comments (3)
packages/core/src/external/internal-server/client.ts (3)
28-33
: Prevent accidental overrides by additionalConfig (method/url/data).additionalConfig currently spreads last and can override method, url, and data. Make explicit args authoritative and narrow the type so callers can’t override them.
- async makeRequest<T = unknown>( - method: "GET" | "POST" | "PUT" | "DELETE", + async makeRequest<T = unknown>( + method: "GET" | "POST" | "PUT" | "DELETE", path: string, data?: unknown, - additionalConfig?: AxiosRequestConfig + additionalConfig?: Omit<AxiosRequestConfig, "method" | "url" | "data"> ): Promise<T> { - const response = await this.client.request({ - method, - url: path, - data, - ...additionalConfig, - }); + const response = await this.client.request({ + ...additionalConfig, + method, + url: path, + data, + });
16-19
: Extract magic number timeout to a named constant.Aligns with “move literals to constants after imports”.
import { Config } from "../../util/config"; +const DEFAULT_TIMEOUT_MS = 20_000; + /** * API client for internal server communication. */ export class InternalServerApi { @@ this.client = axios.create({ baseURL, - timeout: 20_000, + timeout: DEFAULT_TIMEOUT_MS, });
22-27
: Consider future-proofing method and constructor ergonomics.
- If PATCH requests are expected (billing/update endpoints often use PATCH), include it now.
- Optional: allow injecting baseURL for tests to avoid env coupling.
- async makeRequest<T = unknown>( - method: "GET" | "POST" | "PUT" | "DELETE", + async makeRequest<T = unknown>( + method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",- constructor() { - const baseURL = Config.getInternalServerurl(""); + constructor(baseURL?: string) { + const resolvedBaseURL = baseURL ?? Config.getInternalServerurl(""); - if (!baseURL) { + if (!resolvedBaseURL) { throw new MetriportError("INTERNAL_SERVER_BASE_URL not configured"); } this.client = axios.create({ - baseURL, + baseURL: resolvedBaseURL, timeout: 20_000, });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled
- Linear integration is disabled
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
packages/core/src/external/internal-server/client.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{js,jsx,ts,tsx}
: Don’t use null inside the app, only on code interacting with external interfaces/services, like DB and HTTP; convert to undefined before sending inwards into the code
Useconst
whenever possible
Useasync/await
instead of.then()
Naming: classes, enums: PascalCase
Naming: constants, variables, functions: camelCase
Naming: file names: kebab-case
Naming: Don’t use negative names, likenotEnabled
, preferisDisabled
If possible, use decomposing objects for function parameters
Prefer Nullish Coalesce (??) than the OR operator (||) when you want to provide a default value
Avoid creating arrow functions
Use truthy syntax instead ofin
- i.e.,if (data.link)
notif ('link' in data)
While handling errors, keep the stack trace around: if you create a new Error (e.g., MetriportError), make sure to pass the original error as the new one’s cause so the stack trace is available upstream.
max column length is 100 chars
multi-line comments use/** */
top-level comments go after the import (save pre-import to basic file header, like license)
move literals to constants declared after imports when possible
Files:
packages/core/src/external/internal-server/client.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
Use types whenever possible
**/*.{ts,tsx}
: Prefer functional programming style functions: small, deterministic, single input and output
Avoid modifying objects received as parameters
Naming: classes and enums use PascalCase
Naming: constants, variables, and functions use camelCase
File names use kebab-case
Avoid negative names (e.g., prefer isDisabled to notEnabled)
Include units in numeric variable names when the type doesn’t convey the unit
TypeScript: use explicit types
TypeScript: prefer const over let
TypeScript: avoid any and casting from any to other types
TypeScript: use type predicates only for narrowing, not for full type coercion
TypeScript: prefer destructuring function parameters instead of multiple same-typed parameters
TypeScript: don’t use null inside the app; only at external boundaries and convert to undefined internally
TypeScript: use async/await instead of .then()
TypeScript: use strict equality ===; do not use ==
When calling a Promise-returning function without awaiting, chain .catch() to handle errors
Date/Time: always use buildDayjs() to create dayjs instances
Date/Time: prefer dayjs.duration(...) and keep values as duration
Prefer nullish coalescing (??) over OR (||) for default values
Avoid creating arrow functions
Use truthy property checks instead of the in operator (e.g., if (data.link))
Error handling: pass the original error as cause when wrapping errors
Error handling: use static error messages; put dynamic data in MetriportError.additionalInfo; error object last parameter
Global constants: move literals to constants after imports (avoid magic numbers)
Avoid multi-line logs: do not pass objects as the second parameter to console.log() or out().log()
Avoid multi-line logs: do not create multi-line strings with JSON.stringify()
Max column length is 100 characters
Use multi-line comments with /** */
Files:
packages/core/src/external/internal-server/client.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: alexeysergeev-cm
PR: metriport/metriport#4367
File: packages/api/docker-compose.dev.yml:34-34
Timestamp: 2025-08-14T20:13:35.367Z
Learning: In packages/api/docker-compose.dev.yml, CX_BILLING_URL environment variable is not required for development purposes. Only INTERNAL_SERVER_BASE_URL is needed for dev inter-server communication, even though the code has getCxBillingurl("") functionality that may throw if undefined.
🧬 Code graph analysis (1)
packages/core/src/external/internal-server/client.ts (2)
packages/core/src/util/config.ts (1)
Config
(11-408)packages/shared/src/index.ts (1)
MetriportError
(44-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: check-pr / lint-build-test
- GitHub Check: check-pr / lint-build-test
- GitHub Check: check-pr / lint-build-test
- GitHub Check: Analyze (javascript)
🔇 Additional comments (1)
packages/core/src/external/internal-server/client.ts (1)
11-19
: Add guard forINTERNAL_SERVER_BASE_URL
before constructingInternalServerApi
.
- In packages/api/src/command/internal-server/update-customer.ts (line 19) and report-usage.ts (line 18), call
Config.getInternalServerurl("")
and only instantiate when it’s set (e.g.const baseURL = Config.getInternalServerurl(""); if (!baseURL) return;
).⛔ Skipped due to learnings
Learnt from: husainsyed PR: metriport/metriport#3655 File: packages/shared/src/validators/driver-license/index.ts:1-89 Timestamp: 2025-04-10T21:17:35.036Z Learning: MetriportError constructor takes three separate parameters: message (string), cause (optional unknown), and additionalInfo (optional object), not a single object with these properties. The correct instantiation is: new MetriportError(message, cause, additionalInfo).
Issues:
Dependencies
Description
Testing
Release Plan
Summary by CodeRabbit
New Features
Chores
Refactor
Bug Fix / Enhancement