Skip to content

Commit b32234b

Browse files
authored
[MBL-19432][Student] Add submission analytics tracking (#3354)
Test plan: - Manually verify assignment_submit_selected event fires when tapping submit button on assignment details - Manually verify audio_recorder media source is tracked when recording audio submission - Unit tests pass (SubmissionDetailsEmptyContentEffectHandlerTest) refs: MBL-19432 affects: Student release note: none ## Checklist - [x] Follow-up e2e test ticket created or not needed (not needed - analytics only) - [x] Tested in dark mode (not applicable - no UI changes) - [x] Tested in light mode (not applicable - no UI changes) - [x] Test in landscape mode and/or tablet (not applicable - no UI changes) - [x] A11y checked (not applicable - no UI changes) - [ ] Approve from product
1 parent f3d2752 commit b32234b

File tree

37 files changed

+655
-91
lines changed

37 files changed

+655
-91
lines changed

apps/CLAUDE.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,15 @@ Each app has ProGuard rules in `{app}/proguard-rules.txt`
229229
The project uses `PrivateData.merge()` to inject private configuration (API keys, tokens) from `android-vault/private-data/`. These are not in version control.
230230

231231
### Localization
232-
Apps support multiple languages. Translation tags are scanned at build time via `LocaleScanner.getAvailableLanguageTags()`.
232+
Apps support multiple languages. Translation tags are scanned at build time via `LocaleScanner.getAvailableLanguageTags()`.
233+
234+
### Pull Requests
235+
When creating a pull request, use the template located at `/PULL_REQUEST_TEMPLATE` in the repository root. The template includes:
236+
- Test plan description
237+
- Issue references (refs:)
238+
- Impact scope (affects:)
239+
- Release note
240+
- Screenshots table (Before/After)
241+
- Checklist (E2E tests, dark/light mode, landscape/tablet, accessibility, product approval)
242+
243+
Use `gh pr create` with the template to create PRs from the command line.

apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsBehaviour.kt

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ import android.widget.Toast
2424
import androidx.appcompat.app.AlertDialog
2525
import androidx.appcompat.widget.Toolbar
2626
import androidx.fragment.app.FragmentActivity
27+
import android.os.Bundle
2728
import com.instructure.canvasapi2.models.Assignment
2829
import com.instructure.canvasapi2.models.CanvasContext
2930
import com.instructure.canvasapi2.models.Course
3031
import com.instructure.canvasapi2.models.LTITool
3132
import com.instructure.canvasapi2.utils.APIHelper
3233
import com.instructure.canvasapi2.utils.Analytics
3334
import com.instructure.canvasapi2.utils.AnalyticsEventConstants
35+
import com.instructure.canvasapi2.utils.AnalyticsParamConstants
3436
import com.instructure.interactions.Navigation
3537
import com.instructure.interactions.bookmarks.Bookmarker
3638
import com.instructure.pandautils.databinding.FragmentAssignmentDetailsBinding
@@ -64,7 +66,6 @@ class StudentAssignmentDetailsBehaviour (
6466
startVideoCapture: () -> Unit,
6567
onLaunchMediaPicker: () -> Unit,
6668
) {
67-
Analytics.logEvent(AnalyticsEventConstants.SUBMIT_MEDIARECORDING_SELECTED)
6869
val builder = AlertDialog.Builder(activity)
6970
val dialogBinding = DialogSubmissionPickerMediaBinding.inflate(LayoutInflater.from(activity))
7071
val dialog = builder.setView(dialogBinding.root).create()
@@ -108,57 +109,81 @@ class StudentAssignmentDetailsBehaviour (
108109
isStudioEnabled: Boolean,
109110
studioLTITool: LTITool?
110111
) {
112+
Analytics.logEvent(AnalyticsEventConstants.ASSIGNMENT_SUBMIT_SELECTED)
113+
111114
val builder = AlertDialog.Builder(activity)
112115
val dialogBinding = DialogSubmissionPickerBinding.inflate(LayoutInflater.from(activity))
113116
val dialog = builder.setView(dialogBinding.root).create()
114117
val submissionTypes = assignment.getSubmissionTypes()
115118

116119
dialog.setOnShowListener {
120+
val nextAttempt = (assignment.submission?.attempt ?: 0) + 1
117121
setupDialogRow(dialog, dialogBinding.submissionEntryText, submissionTypes.contains(
118122
Assignment.SubmissionType.ONLINE_TEXT_ENTRY)) {
123+
Analytics.logEvent(AnalyticsEventConstants.SUBMIT_TEXTENTRY_SELECTED, Bundle().apply {
124+
putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString())
125+
})
119126
router.navigateToTextEntryScreen(
120127
activity,
121128
course,
122129
assignment.id,
123130
assignment.name.orEmpty(),
131+
attempt = nextAttempt.toLong()
124132
)
125133
}
126134
setupDialogRow(dialog, dialogBinding.submissionEntryWebsite, submissionTypes.contains(
127135
Assignment.SubmissionType.ONLINE_URL)) {
136+
Analytics.logEvent(AnalyticsEventConstants.SUBMIT_URL_SELECTED, Bundle().apply {
137+
putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString())
138+
})
128139
router.navigateToUrlSubmissionScreen(
129140
activity,
130141
course,
131142
assignment.id,
132143
assignment.name.orEmpty(),
133144
null,
134-
false
145+
false,
146+
nextAttempt.toLong()
135147
)
136148
}
137149
setupDialogRow(dialog, dialogBinding.submissionEntryFile, submissionTypes.contains(
138150
Assignment.SubmissionType.ONLINE_UPLOAD)) {
139-
router.navigateToUploadScreen(activity, course, assignment)
151+
Analytics.logEvent(AnalyticsEventConstants.SUBMIT_FILEUPLOAD_SELECTED, Bundle().apply {
152+
putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString())
153+
})
154+
router.navigateToUploadScreen(activity, course, assignment, attempt = nextAttempt.toLong())
140155
}
141156
setupDialogRow(dialog, dialogBinding.submissionEntryMedia, submissionTypes.contains(
142157
Assignment.SubmissionType.MEDIA_RECORDING)) {
158+
Analytics.logEvent(AnalyticsEventConstants.SUBMIT_MEDIARECORDING_SELECTED, Bundle().apply {
159+
putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString())
160+
})
143161
showMediaDialog(activity, binding, recordCallback, startVideoCapture, onLaunchMediaPicker)
144162
}
145163
setupDialogRow(
146164
dialog,
147165
dialogBinding.submissionEntryStudio,
148166
isStudioEnabled
149167
) {
168+
Analytics.logEvent(AnalyticsEventConstants.SUBMIT_STUDIO_SELECTED, Bundle().apply {
169+
putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString())
170+
})
150171
navigateToStudioScreen(activity, course, assignment, studioLTITool)
151172
}
152173
setupDialogRow(dialog, dialogBinding.submissionEntryStudentAnnotation, submissionTypes.contains(
153174
Assignment.SubmissionType.STUDENT_ANNOTATION)) {
175+
Analytics.logEvent(AnalyticsEventConstants.SUBMIT_ANNOTATION_SELECTED, Bundle().apply {
176+
putString(AnalyticsParamConstants.ATTEMPT, nextAttempt.toString())
177+
})
154178
assignment.submission?.id?.let{
155179
router.navigateToAnnotationSubmissionScreen(
156180
activity,
157181
course,
158182
assignment.annotatableAttachmentId,
159183
it,
160184
assignment.id,
161-
assignment.name.orEmpty())
185+
assignment.name.orEmpty(),
186+
nextAttempt.toLong())
162187
}
163188
}
164189
}
@@ -174,15 +199,16 @@ class StudentAssignmentDetailsBehaviour (
174199
}
175200

176201
private fun navigateToStudioScreen(activity: FragmentActivity, canvasContext: CanvasContext, assignment: Assignment, studioLTITool: LTITool?) {
177-
Analytics.logEvent(AnalyticsEventConstants.SUBMIT_STUDIO_SELECTED)
202+
val nextAttempt = (assignment.submission?.attempt ?: 0) + 1
178203
RouteMatcher.route(
179204
activity,
180205
StudioWebViewFragment.makeRoute(
181206
canvasContext,
182207
studioLTITool?.getResourceSelectorUrl(canvasContext, assignment).orEmpty(),
183208
studioLTITool?.name.orEmpty(),
184209
true,
185-
assignment
210+
assignment,
211+
nextAttempt.toLong()
186212
)
187213
)
188214
}

apps/student/src/main/java/com/instructure/student/features/assignments/details/StudentAssignmentDetailsRouter.kt

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ import com.instructure.canvasapi2.models.CanvasContext
2323
import com.instructure.canvasapi2.models.LTITool
2424
import com.instructure.canvasapi2.models.Quiz
2525
import com.instructure.canvasapi2.models.RemoteFile
26-
import com.instructure.canvasapi2.utils.Analytics
27-
import com.instructure.canvasapi2.utils.AnalyticsEventConstants
2826
import com.instructure.pandautils.features.assignments.details.AssignmentDetailsRouter
2927
import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment
3028
import com.instructure.pandautils.features.lti.LtiLaunchFragment
@@ -44,11 +42,13 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() {
4442
activity: FragmentActivity,
4543
canvasContext: CanvasContext,
4644
assignment: Assignment,
47-
mediaUri: Uri
45+
mediaUri: Uri,
46+
attempt: Long,
47+
mediaSource: String?
4848
) {
4949
RouteMatcher.route(
5050
activity,
51-
PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, mediaUri)
51+
PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, mediaUri, attempt, mediaSource)
5252
)
5353
}
5454

@@ -90,12 +90,12 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() {
9090
activity: FragmentActivity,
9191
canvasContext: CanvasContext,
9292
assignment: Assignment,
93-
attemptId: Long?
93+
attemptId: Long?,
94+
attempt: Long
9495
) {
95-
Analytics.logEvent(AnalyticsEventConstants.SUBMIT_FILEUPLOAD_SELECTED)
9696
RouteMatcher.route(
9797
activity,
98-
PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, PickerSubmissionMode.FileSubmission)
98+
PickerSubmissionUploadFragment.makeRoute(canvasContext, assignment, PickerSubmissionMode.FileSubmission, attempt)
9999
)
100100
}
101101

@@ -105,12 +105,12 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() {
105105
assignmentId: Long,
106106
assignmentName: String?,
107107
initialText: String?,
108-
isFailure: Boolean
108+
isFailure: Boolean,
109+
attempt: Long
109110
) {
110-
Analytics.logEvent(AnalyticsEventConstants.SUBMIT_TEXTENTRY_SELECTED)
111111
RouteMatcher.route(
112112
activity,
113-
TextSubmissionUploadFragment.makeRoute(course, assignmentId, assignmentName, initialText, isFailure)
113+
TextSubmissionUploadFragment.makeRoute(course, assignmentId, assignmentName, initialText, isFailure, attempt)
114114
)
115115
}
116116

@@ -120,32 +120,33 @@ class StudentAssignmentDetailsRouter: AssignmentDetailsRouter() {
120120
assignmentId: Long,
121121
assignmentName: String?,
122122
initialUrl: String?,
123-
isFailure: Boolean
123+
isFailure: Boolean,
124+
attempt: Long
124125
) {
125-
Analytics.logEvent(AnalyticsEventConstants.SUBMIT_ONLINEURL_SELECTED)
126126
RouteMatcher.route(
127127
activity,
128-
UrlSubmissionUploadFragment.makeRoute(course, assignmentId, assignmentName, initialUrl, isFailure)
128+
UrlSubmissionUploadFragment.makeRoute(course, assignmentId, assignmentName, initialUrl, isFailure, attempt)
129129
)
130130
}
131131

132132
override fun navigateToAnnotationSubmissionScreen(
133133
activity: FragmentActivity,
134134
canvasContext: CanvasContext,
135135
annotatableAttachmentId: Long,
136-
submissionId: Long,
136+
submissionId: Long,
137137
assignmentId: Long,
138-
assignmentName: String
138+
assignmentName: String,
139+
attempt: Long
139140
) {
140-
Analytics.logEvent(AnalyticsEventConstants.SUBMIT_STUDENT_ANNOTATION_SELECTED)
141141
RouteMatcher.route(
142142
activity,
143143
AnnotationSubmissionUploadFragment.makeRoute(
144144
canvasContext,
145145
annotatableAttachmentId,
146146
submissionId,
147147
assignmentId,
148-
assignmentName
148+
assignmentName,
149+
attempt
149150
)
150151
)
151152
}

apps/student/src/main/java/com/instructure/student/fragment/StudioWebViewFragment.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import javax.inject.Inject
5151
class StudioWebViewFragment : InternalWebviewFragment() {
5252
val assignmentId: Long by LongArg(key = Const.ASSIGNMENT_ID)
5353
val assignmentName: String by StringArg(key = Const.ASSIGNMENT_NAME)
54+
val attempt: Long by LongArg(key = Const.SUBMISSION_ATTEMPT, default = 1L)
5455

5556
@Inject
5657
lateinit var submissionHelper: SubmissionHelper
@@ -146,7 +147,7 @@ class StudioWebViewFragment : InternalWebviewFragment() {
146147
url = StringEscapeUtils.unescapeJava(url)
147148

148149
// Upload the url as a submission
149-
submissionHelper.startStudioSubmission(canvasContext, assignmentId, assignmentName, url)
150+
submissionHelper.startStudioSubmission(canvasContext, assignmentId, assignmentName, url, attempt)
150151

151152
// Close this page
152153
navigation?.popCurrentFragment()
@@ -167,7 +168,7 @@ class StudioWebViewFragment : InternalWebviewFragment() {
167168
}
168169
} else null
169170

170-
fun makeRoute(canvasContext: CanvasContext, url: String, title: String, authenticate: Boolean, assignment: Assignment): Route =
171+
fun makeRoute(canvasContext: CanvasContext, url: String, title: String, authenticate: Boolean, assignment: Assignment, attempt: Long = 1L): Route =
171172
Route(
172173
StudioWebViewFragment::class.java, canvasContext,
173174
canvasContext.makeBundle().apply {
@@ -176,6 +177,7 @@ class StudioWebViewFragment : InternalWebviewFragment() {
176177
putString(Const.ACTION_BAR_TITLE, title)
177178
putString(Const.ASSIGNMENT_NAME, assignment.name)
178179
putLong(Const.ASSIGNMENT_ID, assignment.id)
180+
putLong(Const.SUBMISSION_ATTEMPT, attempt)
179181
})
180182

181183
fun validRoute(route: Route) : Boolean {

apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/SubmissionUtils.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ fun uploadAudioRecording(submissionHelper: SubmissionHelper, file: File, assignm
107107
assignmentId = assignment.id,
108108
assignmentGroupCategoryId = assignment.groupCategoryId,
109109
assignmentName = assignment.name,
110-
mediaFilePath = file.path
110+
mediaFilePath = file.path,
111+
mediaSource = "audio_recorder"
111112
)
112113
}
113114

apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/annnotation/AnnotationSubmissionUploadFragment.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class AnnotationSubmissionUploadFragment : BaseCanvasFragment() {
5151
private var assignmentId by LongArg(key = Const.ASSIGNMENT_ID)
5252
private var canvasContext by ParcelableArg<CanvasContext>(key = Const.CANVAS_CONTEXT)
5353
private var assignmentName by StringArg(key = Const.ASSIGNMENT_NAME)
54+
private var attempt by LongArg(key = Const.SUBMISSION_ATTEMPT, default = 1L)
5455

5556
private val viewModel: AnnotationSubmissionViewModel by viewModels()
5657

@@ -88,7 +89,7 @@ class AnnotationSubmissionUploadFragment : BaseCanvasFragment() {
8889
toolbar.setMenu(R.menu.menu_submit_generic) {
8990
when (it.itemId) {
9091
R.id.menuSubmit -> {
91-
submissionHelper.startStudentAnnotationSubmission(canvasContext, assignmentId, assignmentName, annotatableAttachmentId)
92+
submissionHelper.startStudentAnnotationSubmission(canvasContext, assignmentId, assignmentName, annotatableAttachmentId, attempt)
9293
requireActivity().onBackPressed()
9394
}
9495
}
@@ -111,14 +112,16 @@ class AnnotationSubmissionUploadFragment : BaseCanvasFragment() {
111112
annotatableAttachmentId: Long,
112113
submissionId: Long,
113114
assignmentId: Long,
114-
assignmentName: String
115+
assignmentName: String,
116+
attempt: Long = 1L
115117
): Route {
116118
val bundle = Bundle().apply {
117119
putParcelable(Const.CANVAS_CONTEXT, canvasContext)
118120
putLong(ANNOTATABLE_ATTACHMENT_ID, annotatableAttachmentId)
119121
putLong(SUBMISSION_ID, submissionId)
120122
putLong(Const.ASSIGNMENT_ID, assignmentId)
121123
putString(Const.ASSIGNMENT_NAME, assignmentName)
124+
putLong(Const.SUBMISSION_ATTEMPT, attempt)
122125
}
123126
return Route(AnnotationSubmissionUploadFragment::class.java, canvasContext, bundle)
124127
}

apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadEffectHandler.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,21 @@ class PickerSubmissionUploadEffectHandler(
130130
private fun handleSubmit(model: PickerSubmissionUploadModel) {
131131
when (model.mode) {
132132
MediaSubmission -> {
133+
val file = model.files.first()
134+
val mediaType = when {
135+
file.contentType?.startsWith("video/") == true -> "video"
136+
file.contentType?.startsWith("audio/") == true -> "audio"
137+
else -> null
138+
}
133139
submissionHelper.startMediaSubmission(
134140
canvasContext = model.canvasContext,
135141
assignmentId = model.assignmentId,
136142
assignmentName = model.assignmentName,
137143
assignmentGroupCategoryId = model.assignmentGroupCategoryId,
138-
mediaFilePath = model.files.first().fullPath
144+
mediaFilePath = file.fullPath,
145+
attempt = model.attemptId ?: 1L,
146+
mediaType = mediaType,
147+
mediaSource = model.mediaSource
139148
)
140149
}
141150
FileSubmission -> {
@@ -144,7 +153,8 @@ class PickerSubmissionUploadEffectHandler(
144153
assignmentId = model.assignmentId,
145154
assignmentName = model.assignmentName,
146155
assignmentGroupCategoryId = model.assignmentGroupCategoryId,
147-
files = ArrayList(model.files)
156+
files = ArrayList(model.files),
157+
attempt = model.attemptId ?: 1L
148158
)
149159
}
150160
CommentAttachment -> {

apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadModels.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ data class PickerSubmissionUploadModel(
5050
val mediaFileUri: Uri? = null,
5151
val files: List<FileSubmitObject> = emptyList(),
5252
val isLoadingFile: Boolean = false,
53-
val attemptId: Long? = null
53+
val attemptId: Long? = null,
54+
val mediaSource: String? = null
5455
)
5556

5657
enum class PickerSubmissionMode {

apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadUpdate.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ class PickerSubmissionUploadUpdate :
2626
return if (model.mediaFileUri == null) {
2727
First.first(model)
2828
} else {
29-
First.first(model.copy(isLoadingFile = true), setOf(PickerSubmissionUploadEffect.LoadFileContents(model.mediaFileUri, model.allowedExtensions)))
29+
val source = model.mediaSource ?: "camera"
30+
First.first(model.copy(isLoadingFile = true, mediaSource = source), setOf(PickerSubmissionUploadEffect.LoadFileContents(model.mediaFileUri, model.allowedExtensions)))
3031
}
3132
}
3233

@@ -35,9 +36,9 @@ class PickerSubmissionUploadUpdate :
3536
event: PickerSubmissionUploadEvent
3637
): Next<PickerSubmissionUploadModel, PickerSubmissionUploadEffect> = when (event) {
3738
PickerSubmissionUploadEvent.SubmitClicked -> Next.dispatch(setOf(PickerSubmissionUploadEffect.HandleSubmit(model)))
38-
PickerSubmissionUploadEvent.CameraClicked -> Next.dispatch(setOf(PickerSubmissionUploadEffect.LaunchCamera))
39-
PickerSubmissionUploadEvent.GalleryClicked -> Next.dispatch(setOf(PickerSubmissionUploadEffect.LaunchGallery))
40-
PickerSubmissionUploadEvent.SelectFileClicked -> Next.dispatch(setOf(PickerSubmissionUploadEffect.LaunchSelectFile))
39+
PickerSubmissionUploadEvent.CameraClicked -> Next.next(model.copy(mediaSource = "camera"), setOf(PickerSubmissionUploadEffect.LaunchCamera))
40+
PickerSubmissionUploadEvent.GalleryClicked -> Next.next(model.copy(mediaSource = "library"), setOf(PickerSubmissionUploadEffect.LaunchGallery))
41+
PickerSubmissionUploadEvent.SelectFileClicked -> Next.next(model.copy(mediaSource = "files"), setOf(PickerSubmissionUploadEffect.LaunchSelectFile))
4142
is PickerSubmissionUploadEvent.OnFileSelected -> {
4243
Next.next(
4344
model.copy(isLoadingFile = true),

0 commit comments

Comments
 (0)