Harden date input instrumentation; remove it from front-end boot
Test plan: - build passes flag=none Change-Id: I2130999fc5e762aff704095ea7446bdfc4313c0b Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/327520 Reviewed-by: Charley Kline <ckline@instructure.com> QA-Review: Charley Kline <ckline@instructure.com> Product-Review: Charley Kline <ckline@instructure.com> Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
This commit is contained in:
parent
75d5900d20
commit
f96b5e683d
|
@ -26,7 +26,6 @@ import './initializers/fakeRequireJSFallback'
|
|||
import './initializers/ujsLinks'
|
||||
import {up as configureDateTimeMomentParser} from './initializers/configureDateTimeMomentParser'
|
||||
import {up as configureDateTime} from './initializers/configureDateTime'
|
||||
import {up as enableDTNPI} from './initializers/enableDTNPI'
|
||||
import {initSentry} from './initializers/initSentry'
|
||||
import {up as renderRailsFlashNotifications} from './initializers/renderRailsFlashNotifications'
|
||||
import {up as activateCourseMenuToggler} from './initializers/activateCourseMenuToggler'
|
||||
|
@ -67,10 +66,6 @@ window.addEventListener('canvasReadyStateChange', function ({detail}) {
|
|||
}
|
||||
})
|
||||
|
||||
isolate(enableDTNPI)({
|
||||
endpoint: window.ENV.DATA_COLLECTION_ENDPOINT,
|
||||
})
|
||||
|
||||
// In non-prod environments only, arrange for filtering of "useless" console
|
||||
// messages, and if deprecation reporting is enabled, arrange to inject and
|
||||
// set up Sentry for it.
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-nocheck
|
||||
/*
|
||||
* Copyright (C) 2021 - present Instructure, Inc.
|
||||
*
|
||||
|
@ -17,12 +16,13 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import createPersistentArray from './enableDTNPI.utils'
|
||||
import {createPersistentArray, normalizeEvent, postToBackend} from './enableDTNPI.utils'
|
||||
import type {PersistentArray, NormalizedDTNPIEvent} from './enableDTNPI.utils'
|
||||
import {configure} from '@canvas/datetime-natural-parsing-instrument'
|
||||
|
||||
const localStorageKey = 'dtnpi'
|
||||
const LOCAL_STORAGE_KEY = 'dtnpi'
|
||||
|
||||
let events
|
||||
let events: PersistentArray<NormalizedDTNPIEvent> | null
|
||||
|
||||
export async function up(options = {endpoint: null, throttle: 1000, size: 50}) {
|
||||
const throttle = Math.max(1, Math.min(options.throttle, 1000))
|
||||
|
@ -30,17 +30,16 @@ export async function up(options = {endpoint: null, throttle: 1000, size: 50}) {
|
|||
const {endpoint} = options
|
||||
|
||||
events = createPersistentArray({
|
||||
key: localStorageKey,
|
||||
key: LOCAL_STORAGE_KEY,
|
||||
throttle,
|
||||
size,
|
||||
// @ts-expect-error
|
||||
transform: value => value.map(normalizeEvent),
|
||||
})
|
||||
|
||||
configure({events})
|
||||
|
||||
// submit events that have been collected so far:
|
||||
const collected = [].concat(events)
|
||||
const collected = [...events]
|
||||
|
||||
if (endpoint && collected.length) {
|
||||
await postToBackend({endpoint, events: collected})
|
||||
|
@ -55,32 +54,5 @@ export function down() {
|
|||
events = null
|
||||
}
|
||||
|
||||
localStorage.removeItem(localStorageKey)
|
||||
}
|
||||
|
||||
function normalizeEvent(event) {
|
||||
return {
|
||||
id: event.id,
|
||||
type: 'datepicker_usage',
|
||||
locale: (window.ENV && window.ENV.LOCALE) || null,
|
||||
method: event.method,
|
||||
parsed: event.parsed,
|
||||
// don't store values that may be too long, 32 feels plenty for what people
|
||||
// may actually type
|
||||
value: event.value ? event.value.slice(0, 32) : null,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: submit to an actual backend
|
||||
function postToBackend({endpoint, events}): Promise<void> {
|
||||
// @ts-expect-error
|
||||
return fetch(endpoint, {
|
||||
method: 'PUT',
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(events),
|
||||
})
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEY)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-nocheck
|
||||
/*
|
||||
* Copyright (C) 2023 - present Instructure, Inc.
|
||||
*
|
||||
|
@ -41,19 +40,39 @@ export type PersistentArrayParameters = {
|
|||
// transform parameter. This only applies to the saved value and not for the
|
||||
// one stored in memory.
|
||||
//
|
||||
// Don't mutate it!!!
|
||||
transform: <T>(array: Array<T>) => Array<T>
|
||||
// Don't mutate it
|
||||
transform: (array: NormalizedDTNPIEvent[]) => NormalizedDTNPIEvent[]
|
||||
}
|
||||
|
||||
export type DateTimeInputMethod = 'pick' | 'type' | 'paste'
|
||||
|
||||
export type DTNPIEvent = {
|
||||
id: string
|
||||
method: DateTimeInputMethod
|
||||
parsed: string | null
|
||||
value: string | null
|
||||
}
|
||||
export type NormalizedDTNPIEvent = {
|
||||
id: string
|
||||
type: string
|
||||
locale: string | null
|
||||
method: DateTimeInputMethod
|
||||
parsed: string | null
|
||||
value: string | null
|
||||
}
|
||||
|
||||
// A special Array that persists its value to localStorage whenever you push to,
|
||||
// pop from, or splice it.
|
||||
export default function createPersistentArray({
|
||||
export function createPersistentArray({
|
||||
key,
|
||||
throttle = 1000,
|
||||
size = Infinity,
|
||||
transform = x => x,
|
||||
}: PersistentArrayParameters) {
|
||||
const value = JSON.parse(localStorage.getItem(key) || '[]')
|
||||
transform = x => x as NormalizedDTNPIEvent[],
|
||||
}: PersistentArrayParameters): PersistentArray<NormalizedDTNPIEvent> {
|
||||
const value = JSON.parse(localStorage.getItem(key) || '[]') as NormalizedDTNPIEvent[]
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`Expected ${key} to be an array`)
|
||||
}
|
||||
const save = () => localStorage.setItem(key, JSON.stringify(transform(value)))
|
||||
const resize = () => {
|
||||
if (value.length >= size) {
|
||||
|
@ -76,16 +95,19 @@ export default function createPersistentArray({
|
|||
// nb: we only intend to cover the APIs we're using
|
||||
}
|
||||
|
||||
for (const [method, saveImpl] of Object.entries(saveBehaviors)) {
|
||||
function entries<T extends Record<string, unknown>>(obj: T) {
|
||||
return Object.entries(obj) as Array<[keyof T, T[keyof T]]>
|
||||
}
|
||||
|
||||
for (const [method, saveImpl] of entries(saveBehaviors)) {
|
||||
// define them as properties so that they are not enumerable; we want this
|
||||
// to behave as much like a regular Array as possible
|
||||
Object.defineProperty(value, method, {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
value() {
|
||||
// @ts-expect-error
|
||||
value(...args: any[]) {
|
||||
saveImpl()
|
||||
return Array.prototype[method].apply(this, arguments)
|
||||
return Array.prototype[method].apply(this, args)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -101,6 +123,41 @@ export default function createPersistentArray({
|
|||
}
|
||||
|
||||
const pipe =
|
||||
(...f) =>
|
||||
x =>
|
||||
(...f: Array<(x: unknown) => void>) =>
|
||||
(x?: unknown) =>
|
||||
f.reduce((acc, fx) => fx(acc), x)
|
||||
|
||||
export function normalizeEvent(event: DTNPIEvent): NormalizedDTNPIEvent {
|
||||
return {
|
||||
id: event.id,
|
||||
type: 'datepicker_usage',
|
||||
locale: window.ENV?.LOCALE || null,
|
||||
method: event.method,
|
||||
parsed: event.parsed,
|
||||
// don't store values that may be too long, 32 feels plenty for what people
|
||||
// may actually type
|
||||
value: event.value ? event.value.slice(0, 32) : null,
|
||||
}
|
||||
}
|
||||
|
||||
export function postToBackend({
|
||||
endpoint,
|
||||
events,
|
||||
}: {
|
||||
endpoint: string
|
||||
events: unknown
|
||||
}): Promise<Response> {
|
||||
const url = endpoint || window.ENV.DATA_COLLECTION_ENDPOINT
|
||||
if (!url) {
|
||||
throw new Error('No endpoint provided')
|
||||
}
|
||||
return fetch(url, {
|
||||
method: 'PUT',
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(events),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -16,25 +16,28 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export type InputEvent = {
|
||||
export type DTNPIEvent = {
|
||||
// A unique ID for the widget that generated this event, which can be used
|
||||
// to group related events to get an idea of the whole interaction.
|
||||
id: string
|
||||
locale?: string
|
||||
type: string
|
||||
locale?: string | null
|
||||
method: 'pick' | 'type' | 'paste'
|
||||
// Will be null if the value did not parse into a date.
|
||||
parsed?: string
|
||||
value: string
|
||||
parsed?: string | null
|
||||
value: string | null
|
||||
}
|
||||
|
||||
let state = {events: [] as InputEvent[]}
|
||||
let state: {
|
||||
events: DTNPIEvent[]
|
||||
} = {events: []}
|
||||
|
||||
export function configure({events}: {events: Array<InputEvent>}) {
|
||||
export function configure({events}: {events: Array<DTNPIEvent>}) {
|
||||
const previousState = {...state}
|
||||
state = {events}
|
||||
return previousState
|
||||
}
|
||||
|
||||
export const log = (event: InputEvent): void => {
|
||||
export const log = (event: DTNPIEvent): void => {
|
||||
state.events.push(event)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import tz from '@canvas/timezone'
|
|||
import htmlEscape from 'html-escape'
|
||||
import * as dateFunctions from '../date-functions'
|
||||
import {changeTimezone} from '../changeTimezone'
|
||||
import DatetimeField from './InstrumentedDatetimeField'
|
||||
import DatetimeField from './DatetimeField'
|
||||
import renderDatepickerTime from '../react/components/render-datepicker-time'
|
||||
import '@canvas/keycodes'
|
||||
import 'jqueryui/datepicker'
|
||||
|
|
|
@ -27,8 +27,6 @@ import {DateInput} from '@instructure/ui-date-input'
|
|||
import {IconButton} from '@instructure/ui-buttons'
|
||||
// @ts-ignore
|
||||
import {IconArrowOpenEndSolid, IconArrowOpenStartSolid} from '@instructure/ui-icons'
|
||||
import {nanoid} from 'nanoid'
|
||||
import {log} from '@canvas/datetime-natural-parsing-instrument'
|
||||
import {
|
||||
DateInputInteraction,
|
||||
DateInputLayout,
|
||||
|
@ -170,7 +168,6 @@ export default function CanvasDateInput({
|
|||
const [isShowingCalendar, setIsShowingCalendar] = useState(false)
|
||||
const [renderedMoment, setRenderedMoment] = useState(selectedMoment || todayMoment)
|
||||
const [internalMessages, setInternalMessages] = useState<typeof messages>([])
|
||||
const [widgetId] = useState(nanoid())
|
||||
const [inputDetails, setInputDetails] = useState<{
|
||||
method: 'paste' | 'pick'
|
||||
value: string
|
||||
|
@ -269,9 +266,9 @@ export default function CanvasDateInput({
|
|||
const parsedMoment = moment.tz(date, timezone)
|
||||
let input = parsedMoment
|
||||
if (selectedDate) {
|
||||
const selectedMoment = moment.tz(selectedDate, timezone)
|
||||
if (selectedMoment.isSame(parsedMoment, 'day')) {
|
||||
input = selectedMoment
|
||||
const selectedMoment_ = moment.tz(selectedDate, timezone)
|
||||
if (selectedMoment_.isSame(parsedMoment, 'day')) {
|
||||
input = selectedMoment_
|
||||
}
|
||||
}
|
||||
syncInput(input)
|
||||
|
@ -288,34 +285,9 @@ export default function CanvasDateInput({
|
|||
onSelectedDateChange(newDate, 'other')
|
||||
|
||||
if (inputDetails?.method === 'pick') {
|
||||
const date = inputDetails.value
|
||||
|
||||
setInputDetails(null)
|
||||
|
||||
log({
|
||||
id: widgetId,
|
||||
method: 'pick',
|
||||
parsed: (newDate && newDate.toISOString()) || undefined,
|
||||
value: date,
|
||||
})
|
||||
} else if (inputDetails?.method === 'paste') {
|
||||
const pastedValue = inputDetails.value
|
||||
|
||||
setInputDetails(null)
|
||||
|
||||
log({
|
||||
id: widgetId,
|
||||
method: 'paste',
|
||||
parsed: (newDate && newDate.toISOString()) || undefined,
|
||||
value: pastedValue,
|
||||
})
|
||||
} else if (!inputEmpty) {
|
||||
log({
|
||||
id: widgetId,
|
||||
method: 'type',
|
||||
parsed: (newDate && newDate.toISOString()) || undefined,
|
||||
value: inputValue.trim(),
|
||||
})
|
||||
}
|
||||
onBlur?.(event)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue