Skip to content

feat: dexcom connector - #208

Open
Chin-eng wants to merge 56 commits into
RADAR-base:devfrom
Chin-eng:dexcom_connector
Open

Chin-eng wants to merge 56 commits into
RADAR-base:devfrom
Chin-eng:dexcom_connector

Conversation

@Chin-eng

@Chin-eng Chin-eng commented Aug 3, 2026

Copy link
Copy Markdown

No description provided.

@this-Aditya

Copy link
Copy Markdown
Member

Hi @Chin-eng, I tried compiling this locally, and it failed. It looks like the issue isn’t only that the schemas haven’t been released, there are also some compilation issues even when the schemas are published locally.

Could you please try publishing the schemas locally and fixing the compilation issues first?
Thanks

@Chin-eng Chin-eng closed this Sep 3, 2026
@Chin-eng Chin-eng reopened this Sep 3, 2026
@Chin-eng

Chin-eng commented Sep 3, 2026 •

Copy link
Copy Markdown
Author

Hi @Chin-eng, I tried compiling this locally, and it failed. It looks like the issue isn’t only that the schemas haven’t been released, there are also some compilation issues even when the schemas are published locally.

Could you please try publishing the schemas locally and fixing the compilation issues first? Thanks

I hit the same failure. On my machine, gradle was using a cached radar-schemas-commons:0.9.0 jar without the Dexcom classes. After clearing that cache and compiling against a fresh publishToMavenLocal jar, the BUILD was sucessful.

[Incubating] Problems report is available at: file:///Users/chin-erdenegantulga/Developer/work/radar-base/dexcom/RADAR-REST-Connector/build/reports/problems/problems-report.html
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/8.14/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD SUCCESSFUL in 14s
9 actionable tasks: 8 executed, 1 up-to-date

I used 0.9.1 locally to dodge the stale 0.9.0 cache. Could you run ./gradlew --stop and delete ~/.gradle/caches/modules-2/files-2.1/org.radarbase/radar-schemas-commons, then publish schemas locally and compile again?

This way compilation works on my machine.

@this-Aditya this-Aditya left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @Chin-eng, could you please have a look at the comments inline?

Comment on lines +14 to +16
abstract class DexcomRoute(
private val userRepository: UserRepository,
override val maxIntervalPerRequest: Duration = DEFAULT_INTERVAL_PER_REQUEST,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see every subclass passes base url into this constructor, but it only takes userRepository and a Duration, so the module doesn't compile. Do you meant to add some parameter here for base url?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Sorry, I forgot to stage the DexcomRoute file. I have a lot of unstaged local changes for running the connector locally, and I mixed this file up with those. I added apiBaseUrl.

): Sequence<RestRequest> {
val request = createRequest(
user,
"$DEXCOM_API_BASE_URL/${subPath()}",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This uses the hardcoded url, so requests can never go to sandbox or configurable url, even when the config says sandbox.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah I changed it with the apiBaseUrl but forgot to stage the changes.

Comment on lines +54 to +55
user,
"$DEXCOM_API_BASE_URL/${subPath()}",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here, we can have the configurable base url so we can request any endpoint, eg. sandbox.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

changed it with apiBaseUrl

class DexcomAlertsRoute(
userRepository: UserRepository,
apiBaseUrl: String = DEFAULT_API_BASE_URL,
) : DexcomRoute(userRepository, apiBaseUrl) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We are passing the base url here, The base class request the second argument as duration but we are passing this as a url string.

This is same for every subclass of dexcom route.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah, I added the apiBadeUrl as the second argument for the parent class.

.define(
SOURCE_URL_CONFIG,
Type.STRING,
DexcomRoute.DEFAULT_API_BASE_URL,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't see this defined anywhere.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah I added it inside the new dexcomRoute file.

Comment on lines +15 to +16
private val dataRangeApiBaseUrl: String = DEFAULT_API_BASE_URL,
) : DexcomRoute(userRepository, dataRangeApiBaseUrl) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The Dexcom route won't compile with the apiBaseUrl param, i think it would be better to keep this param in the base class.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I put the apiBaseUrl inside the DexcomRoute

start: Instant,
end: Instant,
): Sequence<RestRequest> {
val request = createRequest(user, "$dataRangeApiBaseUrl/${subPath()}", "")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One the dataRangeApiBaseUrl parameter is shifted to base class, this variable can be renamed.

user: User,
): Sequence<RestRequest> {
val offset = dexcomOffsetManager.getOffset(route, user)
val startDate = user.startDate

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IIRC, right now we can't get backfill data for more than 90 days. Even if we agreed to support 365 days of historical data, it could still fail if the user's start date is more than 365 days, so we should limit this if this is the real case. I am not sure exactly how much we can really backfill now, but please update the logic according to that.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'm now using the dataRange window instead of user.startDate. When there is no saved offset, generateRequests calls dataRangeCache.windowFor and starts from that window's start.

})
.collect(Collectors.toList());
this.configuredUsers = SequencesKt.toSet(dexcomConfig.getUserRepository().stream());
logger.info("Received userTask Configs {}", userTasks);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am not sure, but does userTasks also logs the clent id and secret, we can simply log the task and user counts:

Suggested change
logger.info("Received userTask Configs {}", userTasks);
logger.info("Configured {} tasks for {} users", userTasks.size(), configuredUsers.size());

Comment on lines +172 to +175
val nextOffset = if (dataAge <= Duration.ofDays(7)) {
maxOffsetTime.plus(OFFSET_BUFFER)
} else {
maxOf(maxOffsetTime.plus(OFFSET_BUFFER), request.endDate)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why the next offset is after 12 hours from the newest record, with this logic I think the next 12 hours from the current offset won't be fetched?

@Chin-eng Chin-eng Sep 23, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah, I meant to change it later. I was testing something. I was planning to make the poll wait window every 5 minutes. I will change it.

Comment on lines +125 to +131
fun parseDexcomTime(value: String): Instant {
return try {
OffsetDateTime.parse(value).toInstant()
} catch (_: DateTimeParseException) {
Instant.parse(value)
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This still fails for timestamps without an offset. Dexcom's docs say records sourced from receivers will not have UTC offsets, and even their own /dataRange example uses a value that our parser can't handle right now. The docs say "systemTime is UTC", so we can parse it as UTC. I think this just needs one more try/catch inside the catch block to parse timestamps without a Z, since they are still in UTC:

LocalDateTime.parse(value).toInstant(ZoneOffset.UTC)

Comment on lines +123 to +139
if (window == null) {
logger.info(
"Skip {} for {}: no dataRange window",
route,
user.versionedId,
)
routeNextRequest[routeKey(route, user)] = Instant.now().plus(BACK_OFF_TIME)
return emptySequence()
}
logger.info(
"No offsets found for {} {}, using dataRange start {}",
route,
user.versionedId,
window.start,
)
startOffset = window.start
endDate = minOf(endNow, window.end)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Starting at window.start can go before the participant's startDate, so we collect data from before they enrolled. Could we start from the window but not before user.startDate, and fall back to user.startDate when there is no window?

startOffset = window?.start?.coerceAtLeast(user.startDate) ?: user.startDate
endDate = endNow

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.

2 participants