-
Notifications
You must be signed in to change notification settings - Fork 5
chore: pass timezone through headers #351
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: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,9 +1,9 @@ | ||||||
| import { IMainContext } from '../../core-types'; | ||||||
| import { extractUserFromHeader } from '../headers'; | ||||||
| import { getSubdomain } from '../utils'; | ||||||
| import { ExpressContextFunctionArgument } from '@apollo/server/dist/esm/express4'; | ||||||
| import { Request as ApiRequest, Response as ApiResponse } from 'express'; | ||||||
| import { nanoid } from 'nanoid'; | ||||||
| import { IMainContext } from '../../core-types'; | ||||||
| import { extractUserFromHeader, getTimezone } from '../headers'; | ||||||
| import { getSubdomain } from '../utils'; | ||||||
|
|
||||||
| export const generateApolloContext = | ||||||
| <TContext>( | ||||||
|
|
@@ -22,6 +22,7 @@ export const generateApolloContext = | |||||
| return {}; | ||||||
| } | ||||||
| const user: any = extractUserFromHeader(req.headers); | ||||||
| const timezone: any = getTimezone(req.headers); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use an explicit type (string) for the timezone instead of 'any'.
Suggested change
|
||||||
|
|
||||||
| const subdomain = getSubdomain(req); | ||||||
|
|
||||||
|
|
@@ -39,6 +40,7 @@ export const generateApolloContext = | |||||
| requestInfo: { | ||||||
| secure: req.secure, | ||||||
| cookies: req.cookies, | ||||||
| timezone, | ||||||
| }, | ||||||
| }; | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| export * from './sanitize'; | ||
| export * from './user'; | ||
| export * from './get-hostname'; | ||
| export * from './sanitize'; | ||
| export * from './subdomain'; | ||
| export * from './timezone'; | ||
| export * from './user'; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,5 @@ | ||||||||||||||||||||||||||||||||
| import { IncomingHttpHeaders } from 'http'; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export const getTimezone = (req: IncomingHttpHeaders) => { | ||||||||||||||||||||||||||||||||
| return req['x-timezone'] || req['timezone'] || 'UTC'; | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Consider normalizing timezone header casing and value. Direct string key access may miss headers with different casing. Normalize header keys to lowercase or use a header parsing library. Also, validate that the timezone value is a valid IANA timezone string to avoid errors. |
||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Sanitize, normalize, and validate timezone header IncomingHttpHeaders values can be string | string[] | undefined. Also validate IANA timezones to avoid Mongo $dateToString failures on bad input. -export const getTimezone = (req: IncomingHttpHeaders) => {
- return req['x-timezone'] || req['timezone'] || 'UTC';
-};
+export const getTimezone = (headers: IncomingHttpHeaders): string => {
+ const raw = headers['x-timezone'] ?? headers['timezone'];
+ const value = Array.isArray(raw) ? raw[0] : raw;
+ if (typeof value !== 'string') return 'UTC';
+ try {
+ // Intl throws RangeError for invalid time zones
+ new Intl.DateTimeFormat('en-US', { timeZone: value }).format();
+ return value;
+ } catch {
+ return 'UTC';
+ }
+};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { fillMissingDays } from '@/project/utils/charUtils'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { STATUS_TYPES } from '@/status/constants/types'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { differenceInCalendarDays, startOfDay } from 'date-fns'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { differenceInCalendarDays } from 'date-fns'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { toZonedTime } from 'date-fns-tz'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { Types } from 'mongoose'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { IModels } from '~/connectionResolvers'; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -115,6 +116,7 @@ export const getCycleProgressChart = async ( | |||||||||||||||||||||||||||||||||||||||||||||||
| cycleId: string, | ||||||||||||||||||||||||||||||||||||||||||||||||
| assigneeId: string | undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||
| models: IModels, | ||||||||||||||||||||||||||||||||||||||||||||||||
| timezone: string, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+119
to
120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainProvide a safe default for the timezone parameter. Guard callers that haven’t been migrated by defaulting to 'UTC'. -export const getCycleProgressChart = async (
+export const getCycleProgressChart = async (
cycleId: string,
assigneeId: string | undefined,
models: IModels,
- timezone: string,
+ timezone: string = 'UTC',
) => {Run this to find any remaining call sites not passing a TZ: 🏁 Script executed: #!/bin/bash
rg -n --type=ts 'getCycleProgressChart\(' -C2Length of output: 2668 Default timezone parameter to 'UTC' in getCycleProgressChart Default timezone to 'UTC' so callers that haven't been migrated don't break — resolver already passes timezone, but there is at least one internal call that omits it. -export const getCycleProgressChart = async (
+export const getCycleProgressChart = async (
cycleId: string,
assigneeId: string | undefined,
models: IModels,
- timezone: string,
+ timezone: string = 'UTC',
) => {Remaining caller without TZ: backend/plugins/operation_api/src/modules/cycle/db/models/Cycle.ts:124 (const chartData = await getCycleProgressChart(_id, undefined, models);) 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| const filter: { cycleId: Types.ObjectId; assigneeId?: string } = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| cycleId: new Types.ObjectId(cycleId), | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -177,10 +179,10 @@ export const getCycleProgressChart = async ( | |||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||
| $addFields: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| dayDate: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| $dateFromParts: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| year: { $year: '$statusChangedDate' }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| month: { $month: '$statusChangedDate' }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| day: { $dayOfMonth: '$statusChangedDate' }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| $dateToString: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| format: '%Y-%m-%d', | ||||||||||||||||||||||||||||||||||||||||||||||||
| date: '$statusChangedDate', | ||||||||||||||||||||||||||||||||||||||||||||||||
| timezone, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| isStarted: { $eq: ['$statusType', STATUS_TYPES.STARTED] }, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -210,7 +212,7 @@ export const getCycleProgressChart = async ( | |||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||
| $project: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| _id: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||
| date: { $dateToString: { format: '%Y-%m-%d', date: '$_id' } }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| date: '$_id', | ||||||||||||||||||||||||||||||||||||||||||||||||
| started: 1, | ||||||||||||||||||||||||||||||||||||||||||||||||
| completed: 1, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -238,15 +240,16 @@ export const getCycleProgressChart = async ( | |||||||||||||||||||||||||||||||||||||||||||||||
| chartData: [], | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const start = startOfDay(new Date(cycle.startDate)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const end = startOfDay(new Date(cycle.endDate)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const start = toZonedTime(cycle.startDate, timezone); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const end = toZonedTime(cycle.endDate, timezone); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const days = differenceInCalendarDays(end, start) + 1; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| chartData.chartData = fillMissingDays( | ||||||||||||||||||||||||||||||||||||||||||||||||
| chartDataAggregation, | ||||||||||||||||||||||||||||||||||||||||||||||||
| cycle.startDate, | ||||||||||||||||||||||||||||||||||||||||||||||||
| start, | ||||||||||||||||||||||||||||||||||||||||||||||||
| days, | ||||||||||||||||||||||||||||||||||||||||||||||||
| timezone, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix double timezone conversion when filling gaps You zone start/end, then fillMissingDays zones again, shifting dates twice. Pass an unzoned base into fillMissingDays and keep days computed from zoned bounds. - const start = toZonedTime(cycle.startDate, timezone);
- const end = toZonedTime(cycle.endDate, timezone);
-
- const days = differenceInCalendarDays(end, start) + 1;
-
- chartData.chartData = fillMissingDays(
- chartDataAggregation,
- start,
- days,
- timezone,
- );
+ const startZoned = toZonedTime(cycle.startDate, timezone);
+ const endZoned = toZonedTime(cycle.endDate, timezone);
+
+ const days = differenceInCalendarDays(endZoned, startZoned) + 1;
+
+ chartData.chartData = fillMissingDays(
+ chartDataAggregation,
+ cycle.startDate, // unzoned base; formatting happens in TZ inside fillMissingDays
+ days,
+ timezone,
+ );📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+243
to
249
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid double TZ conversion and DST drift; compute days on zoned midnights and pass an unzoned base into fillMissingDays (TZ-aware).
-import { differenceInCalendarDays } from 'date-fns';
+import { differenceInCalendarDays, startOfDay } from 'date-fns';
import { toZonedTime } from 'date-fns-tz';
@@
- const start = toZonedTime(cycle.startDate, timezone);
- const end = toZonedTime(cycle.endDate, timezone);
-
- const days = differenceInCalendarDays(end, start) + 1;
-
- chartData.chartData = fillMissingDays(chartDataAggregation, start, days);
+ const startZoned = startOfDay(toZonedTime(cycle.startDate, timezone));
+ const endZoned = startOfDay(toZonedTime(cycle.endDate, timezone));
+
+ const days = differenceInCalendarDays(endZoned, startZoned) + 1;
+
+ // Pass unzoned base and timezone to avoid double-shifting inside fillMissingDays
+ chartData.chartData = fillMissingDays(
+ chartDataAggregation,
+ cycle.startDate,
+ days,
+ timezone,
+ );If fillMissingDays isn’t TZ-aware yet, update it like below (outside this file): // backend/plugins/operation_api/src/modules/project/utils/charUtils.ts
import { addDays } from 'date-fns';
import { formatInTimeZone } from 'date-fns-tz';
export function fillMissingDays(
data: { date: string; started: number; completed: number }[],
baseDate: Date,
totalDays = 7,
timezone = 'UTC',
) {
const filled: typeof data = [];
const index = new Map(data.map(d => [d.date, d]));
for (let i = 0; i < totalDays; i++) {
const date = addDays(baseDate, i);
const key = formatInTimeZone(date, timezone, 'yyyy-MM-dd');
const item = index.get(key) ?? filled[filled.length - 1] ?? { started: 0, completed: 0, date: key };
filled.push(item.date === key ? item : { date: key, started: item.started, completed: item.completed });
}
return filled;
} |
||||||||||||||||||||||||||||||||||||||||||||||||
| return chartData; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -35,6 +35,7 @@ const authLink = setContext((_, { headers }) => { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...headers, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| sessioncode: sessionStorage.getItem('sessioncode') || '', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'x-timezone': Intl.DateTimeFormat().resolvedOptions().timeZone, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
35
to
40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Client timezone detection may not be reliable in all environments. Consider adding fallback logic or validation for environments where Intl.DateTimeFormat().resolvedOptions().timeZone is unavailable or inaccurate.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
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
🧩 Analysis chain
Type timezone strictly; avoid any
getTimezone returns a string after validation; carry that type into context.
Please verify IContext['requestInfo'] includes timezone: string.
Also applies to: 25-26, 41-44
🏁 Script executed:
Length of output: 873
🏁 Script executed:
Length of output: 7065
🏁 Script executed:
Length of output: 6772
🏁 Script executed:
Length of output: 1595
Enforce timezone as string across context
getTimezone can return string|string[]; coerce it to a string, declare its return type, and propagate strict typing into the context and core types.
🤖 Prompt for AI Agents