Skip to content

Analytics tracking enhancements - #259

Draft
tombogle wants to merge 40 commits into
mainfrom
analytics-tracking-enhancements
Draft

Analytics tracking enhancements#259
tombogle wants to merge 40 commits into
mainfrom
analytics-tracking-enhancements

Conversation

@tombogle

@tombogle tombogle commented Jan 6, 2026

Copy link
Copy Markdown
Collaborator

This change is Reviewable

@tombogle
tombogle requested a review from andrew-polk January 6, 2026 14:35
@tombogle tombogle self-assigned this Jan 6, 2026
Also, added missing DLL to Installer
Could not load file or assembly 'System.Text.Json, Version=9.0.0.0'
…by tests

Added explicit method to call to report project progress.
Included advances in completed stages in project progress analytics

@andrew-polk andrew-polk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@andrew-polk reviewed 20 files and all commit messages, and made 1 comment.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on @tombogle).


src/SayMore/Model/Project.cs line 208 at r1 (raw file):

					if (personDelta > 0)
						properties["PersonsAdded"] = personDelta.ToString();

I guess you don't care about removal of sessions or persons? (assuming that is possible)

Same for media duration below

Deal with race condition to be able to track progress reliably.
Remove unnecessary tracking of intermediate updates.

@tombogle tombogle left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@tombogle made 1 comment.
Reviewable status: 15 of 20 files reviewed, 1 unresolved discussion (waiting on @andrew-polk).


src/SayMore/Model/Project.cs line 208 at r1 (raw file):

Previously, andrew-polk wrote…

I guess you don't care about removal of sessions or persons? (assuming that is possible)

Same for media duration below

I'm thinking of "progress" as advancing toward some (nebulous) "completion point." While getting rid of cruft can also be a form of progress, it's not the main focus. There are several other things that I could take into account, such as archiving, but that is already a concretely trackable event. My hope is to have an "event" that more-or-less answers the question, did the user make measurable progress toward creation of a finished work during this SayMore session, as opposed to just going in and poking around.

@andrew-polk

Copy link
Copy Markdown
Contributor

src/SayMore/Model/Project.cs line 208 at r1 (raw file):

Previously, tombogle (Tom Bogle) wrote…

I'm thinking of "progress" as advancing toward some (nebulous) "completion point." While getting rid of cruft can also be a form of progress, it's not the main focus. There are several other things that I could take into account, such as archiving, but that is already a concretely trackable event. My hope is to have an "event" that more-or-less answers the question, did the user make measurable progress toward creation of a finished work during this SayMore session, as opposed to just going in and poking around.

👍

@andrew-polk andrew-polk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@andrew-polk reviewed 5 files and all commit messages, and made 1 comment.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on @tombogle).


src/SayMore/Model/Project.cs line 680 at r2 (raw file):

SetAdditionalMetsData

typo
SetAdditionalMetaData

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Comment on lines +953 to +975
public void TrackStatistics(StatisticsViewModel statisticsViewModel)
{
if (_statisticsViewModel != null)
_statisticsViewModel.FinishedGatheringStatisticsForAllFiles -= FinishedGatheringStatistics;

_statisticsViewModel = statisticsViewModel;
_statisticsViewModel.FinishedGatheringStatisticsForAllFiles += FinishedGatheringStatistics;
if (_statisticsViewModel.IsDataUpToDate)
{
// It either finished before we could hook the event, or we hit the race condition.
FinishedGatheringStatistics(_statisticsViewModel, null);
}
}

private void FinishedGatheringStatistics(object sender, EventArgs e)
{
_statisticsViewModel.FinishedGatheringStatisticsForAllFiles -= FinishedGatheringStatistics;
// Check for race condition (see above).
if (_progressStats != null)
return;

_progressStats = new ProgressStats(_statisticsViewModel);
}

Copilot AI Jan 13, 2026

Copy link

Choose a reason for hiding this comment

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

The new analytics tracking functionality (TrackStatistics, ReportProgressIfAny, and ProgressStats class) lacks test coverage. Since the repository has comprehensive test coverage for both Project and StatisticsViewModel classes, similar test coverage should be added for these new analytics-related methods to verify correct tracking behavior and edge cases.

Copilot uses AI. Check for mistakes.
Comment thread src/SayMore/Model/Project.cs Outdated
Comment on lines +970 to +974
// Check for race condition (see above).
if (_progressStats != null)
return;

_progressStats = new ProgressStats(_statisticsViewModel);

Copilot AI Jan 13, 2026

Copy link

Choose a reason for hiding this comment

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

Potential race condition: _progressStats is accessed from multiple contexts (FinishedGatheringStatistics callback, ReportProgressIfAny, and Dispose) without synchronization. While the null check at line 971 helps, a more robust approach would be to use locking or Interlocked operations to ensure thread-safe access to this field, especially since the statistics gatherer runs on a background thread.

Suggested change
// Check for race condition (see above).
if (_progressStats != null)
return;
_progressStats = new ProgressStats(_statisticsViewModel);
// Use atomic initialization to avoid race conditions when multiple threads
// access _progressStats concurrently.
var newProgressStats = new ProgressStats(_statisticsViewModel);
if (Interlocked.CompareExchange(ref _progressStats, newProgressStats, null) != null)
{
// Another thread has already initialized _progressStats; discard this instance.
// If ProgressStats implements IDisposable, it should be disposed here.
}

Copilot uses AI. Check for mistakes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 20 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • src/SayMore/UI/SplashScreenForm.Designer.cs: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/SayMore/Model/Project.cs
Comment on lines +953 to +965
public void TrackStatistics(StatisticsViewModel statisticsViewModel)
{
if (_statisticsViewModel != null)
_statisticsViewModel.FinishedGatheringStatisticsForAllFiles -= FinishedGatheringStatistics;

_statisticsViewModel = statisticsViewModel;
_statisticsViewModel.FinishedGatheringStatisticsForAllFiles += FinishedGatheringStatistics;
if (_statisticsViewModel.IsDataUpToDate)
{
// It either finished before we could hook the event, or we hit the race condition.
FinishedGatheringStatistics(_statisticsViewModel, null);
}
}

Copilot AI Jan 13, 2026

Copy link

Choose a reason for hiding this comment

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

The new TrackStatistics method and analytics tracking logic lack test coverage. Consider adding tests to verify: 1) TrackStatistics is called when StatisticsViewModel is created, 2) ReportProgressIfAny correctly tracks deltas, 3) Analytics.Track is called with expected properties.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We don't generally test Analytics code. Given the asyncronous nature of the statistics collection, it's probably not worth the trouble to introduce potentially flaky tests.

@tombogle tombogle left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@tombogle made 3 comments and resolved 1 discussion.
Reviewable status: all files reviewed, 3 unresolved discussions (waiting on @andrew-polk).


src/SayMore/Model/Project.cs line 680 at r2 (raw file):

Previously, andrew-polk wrote…

SetAdditionalMetsData

typo
SetAdditionalMetaData

It really is METS. It's an acronym for Metadata Encoding and Transmission Standard (https://www.loc.gov/standards/mets/)

Comment thread src/SayMore/Model/Project.cs
Comment on lines +953 to +965
public void TrackStatistics(StatisticsViewModel statisticsViewModel)
{
if (_statisticsViewModel != null)
_statisticsViewModel.FinishedGatheringStatisticsForAllFiles -= FinishedGatheringStatistics;

_statisticsViewModel = statisticsViewModel;
_statisticsViewModel.FinishedGatheringStatisticsForAllFiles += FinishedGatheringStatistics;
if (_statisticsViewModel.IsDataUpToDate)
{
// It either finished before we could hook the event, or we hit the race condition.
FinishedGatheringStatistics(_statisticsViewModel, null);
}
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We don't generally test Analytics code. Given the asyncronous nature of the statistics collection, it's probably not worth the trouble to introduce potentially flaky tests.

@andrew-polk andrew-polk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@andrew-polk resolved 1 discussion and dismissed @copilot[bot] from a discussion.
Reviewable status: all files reviewed, 2 unresolved discussions (waiting on @tombogle).

Copilot AI commented Jan 13, 2026

Copy link
Copy Markdown

@andrew-polk I've opened a new pull request, #261, to work on those changes. Once the pull request is ready, I'll request review from you.

@andrew-polk

Copy link
Copy Markdown
Contributor

Sigh. I don't know why copilot did that. Closing it.

Updated copyright date in license file
Added Crowdin to acknowledgments in aboutBox.htm
WIP: Getting everything to compile...
tombogle and others added 24 commits June 5, 2026 11:22
… constructors

- Replace `EditorBase(ILocalizationManager)` with `protected EditorBase()` that
  reads from `ApplicationContainer.SayMoreLocalizationManager` directly
- Replace 4-arg `EditorBase(ComponentFile, string, string, ILocalizationManager)`
  with 3-arg version delegating to the no-arg constructor
- Use null-guard `if (_localizationManager != null)` for event subscription/
  unsubscription to support unit tests where ApplicationContainer is not initialized
- Fixes "does not contain a constructor that takes 0 arguments" errors in
  MediaComponentEditor, DiagnosticsFileInfoControl, and related subclasses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ager static

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… static

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop ILocalizationManager constructor params and field storage from
FileType base, AnnotationFileWithMissingMediaFileType,
AudioVideoFileTypeBase, AudioFileType, VideoFileType,
OralAnnotationFileType, and ImageFileType.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ubclasses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…call and lm guard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… in 5 EditorBase subclasses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ojectDocsScreen

Drop lm constructor params from ProjectMetadataScreen,
ProjectDocsScreen, ProjectDescriptionDocsScreen, and
ProjectOtherDocsScreen. Remove _localizationManager field and
clean up HandleStringsLocalized and HandleAfterComponentFileSelected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… ShowLocalizationDialogBox with Crowdin URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… ContributorsEditor)

These were not in the original plan but had the same 4-arg base call
and lm guard patterns as Task 8 files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nstructor to OnLoad

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…from EditorBase

tabText was always null at every call site; Initialize only set ImageKey (after
a no-op TabText assignment). Replace the 3-param EditorBase constructor with a
2-param one that sets ImageKey directly, remove Initialize from both the
IEditorProvider interface and the class body, and update all 18 subclasses
(including MediaComponentEditor and its two subclasses) to drop the null arg.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts:
#	build/TestInstallerBuild.bat
# Conflicts:
#	src/SayMore/Model/Project.cs
#	src/SayMore/UI/ProjectChoosingAndCreating/WelcomeDialog.cs
The custom PrivacyDlg/CustomAction implementation was broken (dialog
navigation referenced property names that didn't match the actual
property definitions) and is now fully superseded by SIL.Installer's
WiX-native analytics fragment, wired in via a single ComponentGroupRef.

Also bumps all SIL.* PackageReferences to the latest 18.0.0-beta0032
(from the mismatched beta0026/beta0032 split) to keep the libpalaso
package family in lockstep, and adds a restore step in build/SayMore.proj
for Installer.wixproj's new PackageReference-based dependency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rning

v3 runs on Node 24; same behavior otherwise, per the release notes.
…ld versions

The computed version was never making it into the msbuild /p:Version
argument (only exported as a step output, never copied into $env:Version),
so every build silently fell back to the 1.0.0 default in SayMore.proj.

Also replaces the old X.Y.Z.<commit-count> scheme for non-tag builds with
an automatic one that needs no manual tag pushes: same major.minor as the
last real release, patch number at least 1000 higher and still increasing
with every commit, so test builds always sort above production releases
without colliding with the eventual next release's version.
Fixed regression bug with gender combo box
Corrected Spanish combo box items for Male and Female
Brought license file up to date
@tombogle
tombogle requested a review from andrew-polk August 14, 2026 23:26
@tombogle

Copy link
Copy Markdown
Collaborator Author

@andrew-polk You can re-look at this now or wait. There's (at least) one more small defect to address, but I'm out of time an tokens for now.

@tombogle
tombogle marked this pull request as draft August 14, 2026 23:32
… so Privacy dialog can be opened in a DEBUG build
@tombogle

Copy link
Copy Markdown
Collaborator Author

On flag from Devin that I'm not addressing in code but is worth a comment:
src/SayMore/UI/ComponentEditors/PersonBasicEditor.cs:R880-895

EditorBase constructor calls virtual HandleStringsLocalized moved to OnLoad

Previously EditorBase() called HandleStringsLocalized(null) in the constructor; now it subscribes to _localizationManager.UiLanguageChanged and the initial localization is performed in OnLoad (src/SayMore/UI/ComponentEditors/EditorBase.cs:173). Several derived classes still call HandleStringsLocalized(null, EventArgs.Empty) explicitly from their own constructors (e.g. MissingMediaFileEditor, AudioVideoPlayer via FinishInitializing). PersonBasicEditor removed its constructor call to HandleStringsLocalized(null) and relies on OnLoad. Worth confirming that gender combo population (which now happens in OnLoad via HandleStringsLocalized and in HandleBinderTranslateBoundValueBeingRetrieved via the _pendingSelectedGenderIndex deferral) still yields the correct selected gender for the first person loaded, since binding retrieval can happen before OnLoad. The new _pendingSelectedGenderIndex mechanism appears designed exactly for this ordering, but it's a subtle interaction worth a targeted test.

RESPONSE:
I traced this all the way through rather than just eyeballing it, since it's exactly the same mechanism that had a real bug in the sibling finding you already had me fix. This one, though, checks out — no bug.

The key fact: PersonBasicEditor.Designer.cs never adds any items to _gender.Items in InitializeComponent() (confirmed by grep — the designer only sets DropDownStyle, binding flags, size/location, never .Items.Add(...)). So _gender.Items.Count is guaranteed to be 0 at the moment the constructor runs _binder.SetComponentFile(file).

Tracing the sequence for the first person loaded:

Constructor calls _binder.SetComponentFile(file) → fires TranslateBoundValueBeingRetrieved → HandleBinderTranslateBoundValueBeingRetrieved sees _gender.Items.Count == 0 < 2 → takes the defer branch, sets _pendingSelectedGenderIndex. (This is the same if/else I just restored for the person-switching bug — it correctly takes the other branch here.)
Control gets added to its parent; WinForms guarantees OnLoad fires strictly after the constructor returns, on the same UI thread — there's no background-thread race here the way there was in the Project.cs finding, so ordering is deterministic, not probabilistic.
EditorBase.OnLoad → HandleStringsLocalized → populates the two items and applies _pendingSelectedGenderIndex → correct gender shown for the first person.
So the _pendingSelectedGenderIndex mechanism is doing exactly what Devin speculated it was designed for, and the ordering guarantee comes from WinForms' control lifecycle (constructor always completes before OnLoad can fire), not from luck. No code change needed here — I didn't add a speculative test for it since there's nothing broken to pin down, and this codebase doesn't have infrastructure for instantiating PersonBasicEditor with real controls in a unit test (confirmed when I looked into this for the sibling bug).

Replaced errant bogus 1.2.3.4 version in Installer.wxs
Fixed potential i18n problem with stage labels
Prevented Statistics completion notification from crashing the app after a project is closed
Changed ProgressStats to use the role ID (rather than Name) for gathering media-duration stats, since Name depends on locale and is not stable
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.

4 participants