{"record":{"id":"44461eb526eac5db","repo":"instructure/canvas-lms","slug":"can-t-delete-a-non-existent-observer-for-observer-observer","errorCode":null,"errorMessage":"Can't delete a non-existent observer for observer: #{observer_id}, student: #{student_id}","messagePattern":"Can't delete a non-existent observer for observer: #(.+?), student: #(.+?)","errorType":"validation","errorClass":"ImportError","httpStatus":null,"severity":"error","filePath":"lib/sis/user_observer_importer.rb","lineNumber":86,"sourceCode":"\n        observer = o_pseudo.user\n        student = s_pseudo.user\n        raise ImportError, \"Can't observe yourself user #{student_id}\" if observer == student\n\n        add_remove_observer(observer, student, observer_id, student_id, status)\n      end\n\n      def add_remove_observer(observer, student, observer_id, student_id, status)\n        case status.downcase\n        when \"active\"\n          check_observer_notification_settings(observer)\n          user_observer = UserObservationLink.create_or_restore(observer:, student:, root_account: @root_account)\n        when \"deleted\"\n          user_observer = observer.as_observer_observation_links.for_root_accounts(@root_account).find_by(user_id: student)\n          if user_observer\n            user_observer.destroy\n          else\n            raise ImportError, \"Can't delete a non-existent observer for observer: #{observer_id}, student: #{student_id}\"\n          end\n        end\n        raise ImportError, \"Failed to return user observer for observer: #{observer_id}, student: #{student_id}\" unless user_observer\n\n        @users_to_update_account_associations.add observer.id\n        @user_observers_to_update_sis_batch_ids << user_observer.id\n        @success_count += 1\n      end\n\n      def check_observer_notification_settings(observer)\n        if @root_account.settings[:default_notifications_disabled_for_observers]\n          observer.default_notifications_disabled = true\n          observer.save if observer.changed?\n        end\n      end\n    end\n  end\nend","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/sis/user_observer_importer.rb#L68-L104","documentation":"In add_remove_observer, a status of 'deleted' looks up the observer's existing UserObservationLink scoped to the root account for the given student. If no such link exists there is nothing to delete, so the importer raises ImportError. Only 'active' (create) and 'deleted' (destroy) statuses are supported, and deletes require an existing link.","triggerScenarios":"A user_observers.csv row with status=deleted where the observer was never linked to that student via this root account: the link was already removed by a previous import, the link was created under a different root account/shard, the observer_id or student_id in the delete row differs from the one used when creating the link, or duplicate delete rows in one batch.","commonSituations":"Re-processing an old SIS batch after the link was already deleted, exporting 'deleted' rows from a source system without checking current state, ID normalization differences between the create and delete files, multi-shard setups where find_by(user_id: student) runs on the wrong root account.","solutions":["Verify the UserObservationLink exists before importing the delete row (observer.as_observer_observation_links.for_root_accounts(root_account).find_by(user_id: student)).","Remove the delete row if the link was already deleted in a prior batch — the delete is idempotent-unfriendly by design.","Ensure the observer_id/student_id in the delete row exactly match those used in the original 'active' row.","Check the link's root_account/shard; re-import against the account where the link actually lives."],"exampleFix":"// before (delete for a link that never existed)\nuser_id,associated_user_id,status\nuser:obs1,user:studp99,deleted\n\n// after (create first, then delete)\nuser:obs1,user:studp99,active   # import 1\nuser:obs1,user:studp99,deleted  # import 2 (only if still linked)","handlingStrategy":"validation","validationCode":"# only emit delete rows for links that currently exist\nlink = observer.as_observer_observation_links\n       .for_root_accounts(root_account).find_by(user_id: student)\nnext unless link # skip delete rows with nothing to delete","typeGuard":null,"tryCatchPattern":"begin\n  importer.process_user_observer(obs_id, stud_id, 'deleted')\nrescue SIS::ImportError => e\n  Rails.logger.info(\"already unlinked, ignoring: #{e.message}\")\nend","preventionTips":["Track which links were already deleted to avoid duplicate delete rows","Use identical observer_id/student_id values as the original create row","Verify link's root_account matches the import's root account","Diff source-system state against Canvas before generating delete files"],"tags":["sis","import","record-not-found","ruby"],"backgroundTag":"record-not-found","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}