instructure/canvas-lms · error · ImportError

Can't delete a non-existent observer for observer: #

Error message

Can't delete a non-existent observer for observer: #{observer_id}, student: #{student_id}

What it means

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.

Solutions

  1. Verify the UserObservationLink exists before importing the delete row (observer.as_observer_observation_links.for_root_accounts(root_account).find_by(user_id: student)).
  2. Remove the delete row if the link was already deleted in a prior batch — the delete is idempotent-unfriendly by design.
  3. Ensure the observer_id/student_id in the delete row exactly match those used in the original 'active' row.
  4. Check the link's root_account/shard; re-import against the account where the link actually lives.

Example fix

// before (delete for a link that never existed)
user_id,associated_user_id,status
user:obs1,user:studp99,deleted

// after (create first, then delete)
user:obs1,user:studp99,active   # import 1
user:obs1,user:studp99,deleted  # import 2 (only if still linked)
Defensive patterns

Strategy: validation

Validate before calling

# only emit delete rows for links that currently exist
link = observer.as_observer_observation_links
       .for_root_accounts(root_account).find_by(user_id: student)
next unless link # skip delete rows with nothing to delete

Try / catch

begin
  importer.process_user_observer(obs_id, stud_id, 'deleted')
rescue SIS::ImportError => e
  Rails.logger.info("already unlinked, ignoring: #{e.message}")
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/44461eb526eac5db. Report an issue: GitHub.

Appendix: source

Thrown at lib/sis/user_observer_importer.rb:86

        observer = o_pseudo.user
        student = s_pseudo.user
        raise ImportError, "Can't observe yourself user #{student_id}" if observer == student

        add_remove_observer(observer, student, observer_id, student_id, status)
      end

      def add_remove_observer(observer, student, observer_id, student_id, status)
        case status.downcase
        when "active"
          check_observer_notification_settings(observer)
          user_observer = UserObservationLink.create_or_restore(observer:, student:, root_account: @root_account)
        when "deleted"
          user_observer = observer.as_observer_observation_links.for_root_accounts(@root_account).find_by(user_id: student)
          if user_observer
            user_observer.destroy
          else
            raise ImportError, "Can't delete a non-existent observer for observer: #{observer_id}, student: #{student_id}"
          end
        end
        raise ImportError, "Failed to return user observer for observer: #{observer_id}, student: #{student_id}" unless user_observer

        @users_to_update_account_associations.add observer.id
        @user_observers_to_update_sis_batch_ids << user_observer.id
        @success_count += 1
      end

      def check_observer_notification_settings(observer)
        if @root_account.settings[:default_notifications_disabled_for_observers]
          observer.default_notifications_disabled = true
          observer.save if observer.changed?
        end
      end
    end
  end
end

View on GitHub (pinned to 1c9f0bb801)